mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-09 20:12:14 +10:00
* feat: small refactor * fix: appimage build script * fix: add NO_STRIP to AppImage build * fix: build and dev mode from refactor * fix: submodule step 1 * fix: submodules step 2
301 lines
12 KiB
Vue
301 lines
12 KiB
Vue
<template>
|
|
<div class="border-b border-zinc-700 py-5">
|
|
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
|
Downloads
|
|
</h3>
|
|
</div>
|
|
|
|
|
|
<div class="mt-5">
|
|
<div class="border-b border-zinc-600">
|
|
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
|
<div class="ml-4 mt-2 pb-4">
|
|
<h3 class="text-base font-display font-semibold text-zinc-100">
|
|
Install directories
|
|
</h3>
|
|
<p class="mt-1 text-sm text-zinc-400 max-w-xl">
|
|
This is where Drop will download game files to, and store them
|
|
indefinitely while you play. Drop and games may store other
|
|
information elsewhere, like saves or mods.
|
|
</p>
|
|
</div>
|
|
<div class="ml-4 mt-2 shrink-0">
|
|
<button @click="() => (open = true)" type="button"
|
|
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
|
|
Add new directory
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul role="list" class="divide-y divide-gray-800">
|
|
<li v-for="(dir, dirIdx) in dirs" :key="dir" class="flex justify-between gap-x-6 py-5">
|
|
<div class="flex min-w-0 gap-x-4">
|
|
<FolderIcon class="h-6 w-6 text-blue-600 flex-none rounded-full" alt="" />
|
|
<div class="min-w-0 flex-auto">
|
|
<p class="text-sm/6 text-zinc-100">
|
|
{{ dir }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex shrink-0 items-center gap-x-6">
|
|
<button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[
|
|
dirs.length <= 1
|
|
? 'text-zinc-700'
|
|
: 'text-zinc-400 hover:text-zinc-100',
|
|
'-m-2.5 block p-2.5',
|
|
]">
|
|
<span class="sr-only">Open options</span>
|
|
<TrashIcon class="size-5" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div class="border-t border-zinc-600 py-6">
|
|
<h3 class="text-base font-display font-semibold text-zinc-100">
|
|
Download Settings
|
|
</h3>
|
|
<p class="mt-1 text-sm text-zinc-400 max-w-xl">
|
|
Configure how Drop downloads games and other content.
|
|
</p>
|
|
|
|
<div class="mt-6 max-w-xl">
|
|
<label for="threads" class="block text-sm font-medium text-zinc-100">
|
|
Maximum Download Threads
|
|
</label>
|
|
<div class="mt-2">
|
|
<input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads"
|
|
@keypress="validateNumberInput" @paste="validatePaste"
|
|
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" />
|
|
</div>
|
|
<p class="mt-2 text-sm text-zinc-400">
|
|
The maximum number of concurrent download threads. Higher values may
|
|
download faster but use more system resources. Default is 4.
|
|
</p>
|
|
</div>
|
|
<div class="mt-10 space-y-8">
|
|
<div class="flex flex-row items-center justify-between">
|
|
<div>
|
|
<h3 class="text-sm font-medium leading-6 text-zinc-100">Force Offline</h3>
|
|
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
|
Drop will not make any external connections
|
|
</p>
|
|
</div>
|
|
<Switch v-model="forceOffline" :class="[
|
|
forceOffline ? 'bg-blue-600' : 'bg-zinc-700',
|
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
|
|
]">
|
|
<span :class="[
|
|
forceOffline ? 'translate-x-5' : 'translate-x-0',
|
|
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
|
|
]" />
|
|
</Switch>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="mt-6">
|
|
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
|
|
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
|
saveState.success
|
|
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
|
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
|
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
|
]">
|
|
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<TransitionRoot as="template" :show="open">
|
|
<Dialog class="relative z-50" @close="open = false">
|
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
|
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
|
<div class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity" />
|
|
</TransitionChild>
|
|
|
|
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
<TransitionChild as="template" enter="ease-out duration-300"
|
|
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
|
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
|
<DialogPanel
|
|
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
|
<div class="sm:flex sm:items-start">
|
|
<div class="mt-3 w-full sm:ml-4 sm:mt-0">
|
|
<div>
|
|
<label for="dir" class="block text-sm/6 font-medium text-zinc-100">Select game directory</label>
|
|
<div class="mt-2">
|
|
<button @click="() => selectDirectory()"
|
|
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6">
|
|
{{
|
|
currentDirectory ?? "Click to select a directory..."
|
|
}}
|
|
</button>
|
|
</div>
|
|
<p class="mt-2 text-sm text-zinc-400" id="dir-description">
|
|
Select an empty directory to add.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
<LoadingButton :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading"
|
|
@click="() => submitDirectory()" :class="[
|
|
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
|
|
currentDirectory === undefined
|
|
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
|
|
: 'text-white bg-blue-600 hover:bg-blue-500',
|
|
]">
|
|
Add
|
|
</LoadingButton>
|
|
<button type="button"
|
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
|
@click="() => cancelDirectory()" ref="cancelButtonRef">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-600">
|
|
{{ error }}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</TransitionRoot>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
Dialog,
|
|
DialogPanel,
|
|
TransitionChild,
|
|
TransitionRoot,
|
|
} from "@headlessui/vue";
|
|
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { Switch } from '@headlessui/vue'
|
|
import { type Settings } from "~/types";
|
|
|
|
const open = ref(false);
|
|
const currentDirectory = ref<string | undefined>(undefined);
|
|
const error = ref<string | undefined>(undefined);
|
|
const createDirectoryLoading = ref(false);
|
|
|
|
const dirs = ref<Array<string>>([]);
|
|
|
|
const settings = await invoke<Settings>("fetch_settings");
|
|
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
|
const forceOffline = ref(settings?.forceOffline ?? false);
|
|
|
|
const saveState = reactive({
|
|
loading: false,
|
|
success: false
|
|
});
|
|
|
|
async function updateDirs() {
|
|
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
|
|
dirs.value = newDirs;
|
|
}
|
|
|
|
await updateDirs();
|
|
|
|
async function selectDirectoryDialog(): Promise<string> {
|
|
const res = await invoke("plugin:dialog|open", {
|
|
options: { directory: true },
|
|
});
|
|
|
|
return res as string;
|
|
}
|
|
|
|
async function selectDirectory() {
|
|
try {
|
|
const dir = await selectDirectoryDialog();
|
|
currentDirectory.value = dir;
|
|
} catch (e) {
|
|
error.value = e as string;
|
|
}
|
|
}
|
|
|
|
function cancelDirectory() {
|
|
open.value = false;
|
|
currentDirectory.value = undefined;
|
|
}
|
|
|
|
async function submitDirectory() {
|
|
try {
|
|
error.value = undefined;
|
|
if (!currentDirectory.value)
|
|
throw new Error("Please select a directory first");
|
|
createDirectoryLoading.value = true;
|
|
|
|
// Add directory
|
|
await invoke("add_download_dir", { newDir: currentDirectory.value });
|
|
|
|
// Update list
|
|
await updateDirs();
|
|
|
|
currentDirectory.value = undefined;
|
|
createDirectoryLoading.value = false;
|
|
open.value = false;
|
|
} catch (e) {
|
|
error.value = e as string;
|
|
createDirectoryLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function deleteDirectory(index: number) {
|
|
await invoke("delete_download_dir", { index });
|
|
await updateDirs();
|
|
}
|
|
|
|
async function saveSettings() {
|
|
try {
|
|
saveState.loading = true;
|
|
await invoke("update_settings", {
|
|
newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value },
|
|
});
|
|
|
|
// Show success state
|
|
saveState.success = true;
|
|
|
|
// Reset back to normal state after 2 seconds
|
|
setTimeout(() => {
|
|
saveState.success = false;
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to save settings:', error);
|
|
} finally {
|
|
saveState.loading = false;
|
|
}
|
|
}
|
|
|
|
function validateNumberInput(event: KeyboardEvent) {
|
|
// Allow only numbers and basic control keys
|
|
if (!/^\d$/.test(event.key) &&
|
|
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
function validatePaste(event: ClipboardEvent) {
|
|
// Prevent paste if content contains non-numeric characters
|
|
const pastedData = event.clipboardData?.getData('text');
|
|
if (pastedData && !/^\d+$/.test(pastedData)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
</script>
|