mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-01-30 19:15:17 +01:00
Compare commits
3 Commits
importexpo
...
async
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a85f68d3a | ||
|
|
aad2d964eb | ||
|
|
f92ec38231 |
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
@@ -51,54 +51,19 @@ jobs:
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-latest'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -t 3600 -u build.keychain
|
||||
|
||||
curl https://droposs.org/drop.crt --output drop.pem
|
||||
sudo security authorizationdb write com.apple.trust-settings.user allow
|
||||
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
|
||||
sudo security authorizationdb remove com.apple.trust-settings.user
|
||||
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported. Using identity: $CERT_ID"
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
NO_STRIP: true
|
||||
with:
|
||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'Auto-release v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: ${{ matrix.args }}
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -26,7 +26,4 @@ dist-ssr
|
||||
.output
|
||||
|
||||
src-tauri/flamegraph.svg
|
||||
src-tauri/perf*
|
||||
|
||||
/*.AppImage
|
||||
/squashfs-root
|
||||
src-tauri/perf*
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,6 +1,9 @@
|
||||
[submodule "drop-base"]
|
||||
path = drop-base
|
||||
url = https://github.com/drop-oss/drop-base
|
||||
[submodule "src-tauri/tailscale/libtailscale"]
|
||||
path = src-tauri/tailscale/libtailscale
|
||||
url = https://github.com/tailscale/libtailscale.git
|
||||
[submodule "libs/drop-base"]
|
||||
path = libs/drop-base
|
||||
url = https://github.com/drop-oss/drop-base.git
|
||||
[submodule "src-tauri/umu/umu-launcher"]
|
||||
path = src-tauri/umu/umu-launcher
|
||||
url = https://github.com/Open-Wine-Components/umu-launcher.git
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator color="#2563eb" />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
@@ -10,6 +9,8 @@
|
||||
import "~/composables/downloads.js";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AppStatus } from "~/types";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useAppState } from "./composables/app-state.js";
|
||||
import {
|
||||
initialNavigation,
|
||||
@@ -19,26 +20,19 @@ import {
|
||||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
console.log(state.value)
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
|
||||
async function fetchState() {
|
||||
router.beforeEach(async () => {
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
if (!state.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `App state is: ${state.value}`,
|
||||
fatal: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await fetchState();
|
||||
|
||||
// This is inefficient but apparently we do it lol
|
||||
router.beforeEach(async () => {
|
||||
await fetchState();
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 6.5 MiB |
55
build.mjs
55
build.mjs
@@ -1,55 +0,0 @@
|
||||
import fs from "fs";
|
||||
import process from "process";
|
||||
import childProcess from "child_process";
|
||||
import createLogger from "pino";
|
||||
|
||||
const OUTPUT = "./.output";
|
||||
const logger = createLogger({ transport: { target: "pino-pretty" } });
|
||||
|
||||
async function spawn(exec, opts) {
|
||||
const output = childProcess.spawn(exec, { ...opts, shell: true });
|
||||
output.stdout.on("data", (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
output.stderr.on("data", (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
output.on("error", (err) => reject(err));
|
||||
output.on("exit", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
const expectedLibs = ["drop-base/package.json"];
|
||||
|
||||
for (const lib of expectedLibs) {
|
||||
const path = `./libs/${lib}`;
|
||||
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
|
||||
}
|
||||
|
||||
const views = fs.readdirSync(".").filter((view) => {
|
||||
const expectedPath = `./${view}/package.json`;
|
||||
return fs.existsSync(expectedPath);
|
||||
});
|
||||
|
||||
fs.mkdirSync(OUTPUT, { recursive: true });
|
||||
|
||||
for (const view of views) {
|
||||
const loggerChild = logger.child({});
|
||||
process.chdir(`./${view}`);
|
||||
|
||||
loggerChild.info(`Install deps for "${view}"`);
|
||||
await spawn("yarn");
|
||||
|
||||
loggerChild.info(`Building "${view}"`);
|
||||
await spawn("yarn build", {
|
||||
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
|
||||
});
|
||||
|
||||
process.chdir("..");
|
||||
|
||||
fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
@@ -1,78 +1,52 @@
|
||||
<template>
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu
|
||||
v-if="showDropdown"
|
||||
as="div"
|
||||
class="relative inline-block text-left grow"
|
||||
>
|
||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
||||
<div class="h-full">
|
||||
<MenuButton
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<MenuButton :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<MenuItems
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
|
||||
>
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem v-if="showOptions" v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('options')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button @click="() => emit('options')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]">
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('uninstall')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
<button @click="() => emit('uninstall')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]">
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
@@ -87,7 +61,6 @@ import {
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
ServerIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
@@ -105,7 +78,7 @@ const emit = defineEmits<{
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
(e: "options"): void;
|
||||
(e: "resume"): void;
|
||||
(e: "resume"): void
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(
|
||||
@@ -115,10 +88,6 @@ const showDropdown = computed(
|
||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
||||
);
|
||||
|
||||
const showOptions = computed(
|
||||
() => props.status.type === GameStatusEnum.Installed
|
||||
);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
@@ -126,8 +95,6 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Downloading]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Validating]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]:
|
||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
||||
[GameStatusEnum.Installed]:
|
||||
@@ -139,45 +106,42 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Running]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.PartiallyInstalled]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Install",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading",
|
||||
[GameStatusEnum.Validating]: "Validating",
|
||||
[GameStatusEnum.SetupRequired]: "Setup",
|
||||
[GameStatusEnum.Installed]: "Play",
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Stop",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume"
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Queued]: QueueListIcon,
|
||||
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Validating]: ServerIcon,
|
||||
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
||||
[GameStatusEnum.Installed]: PlayIcon,
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: StopIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.Remote]: () => emit("install"),
|
||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||
[GameStatusEnum.Validating]: () => emit("queue"),
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Uninstalling]: () => { },
|
||||
[GameStatusEnum.Running]: () => emit("kill"),
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
|
||||
};
|
||||
</script>
|
||||
@@ -37,7 +37,7 @@
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
</li>
|
||||
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
||||
<OfflineHeaderWidget v-if="state.status === AppStatus.Offline" />
|
||||
<HeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
||||
<Menu v-if="state.user" as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<HeaderWidget>
|
||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||
@@ -23,7 +23,7 @@
|
||||
<MenuItems
|
||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||
>
|
||||
<div class="flex-col gap-y-2">
|
||||
<PanelWidget class="flex-col gap-y-2">
|
||||
<NuxtLink
|
||||
to="/id/me"
|
||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||
@@ -37,7 +37,7 @@
|
||||
</NuxtLink>
|
||||
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||
<div class="flex flex-col mb-1">
|
||||
<MenuItem v-if="state.user.admin" v-slot="{ active }">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
:href="adminUrl"
|
||||
target="_blank"
|
||||
@@ -65,7 +65,7 @@
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</PanelWidget>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
@@ -87,7 +87,7 @@ router.afterEach(() => {
|
||||
|
||||
const state = useAppState();
|
||||
const profilePictureUrl: string = await useObject(
|
||||
state.value?.user?.profilePictureObjectId ?? ""
|
||||
state.value.user?.profilePictureObjectId ?? ""
|
||||
);
|
||||
const adminUrl: string = await invoke("gen_drop_url", {
|
||||
path: "/admin",
|
||||
@@ -13,7 +13,11 @@
|
||||
<div class="max-w-lg">
|
||||
<slot />
|
||||
<div class="mt-10">
|
||||
<div>
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
||||
>
|
||||
<div v-if="loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -33,19 +37,10 @@
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span class="inline-flex gap-x-8 items-center" v-else>
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
|
||||
>
|
||||
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</button>
|
||||
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
||||
Use a code →
|
||||
</NuxtLink>
|
||||
<span v-else>
|
||||
Sign in with your browser <span aria-hidden="true">→</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
@@ -126,7 +121,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const loading = ref(false);
|
||||
194
components/LibrarySearch.vue
Normal file
194
components/LibrarySearch.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
>
|
||||
<MagnifyingGlassIcon
|
||||
class="h-5 w-5 text-zinc-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="() => calculateGames(true)"
|
||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
||||
>
|
||||
<ArrowPathIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||
<NuxtLink
|
||||
v-for="nav in filteredNavigation"
|
||||
:key="nav.id"
|
||||
:class="[
|
||||
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||
nav.index === currentNavigation
|
||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||
: nav.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="nav.route"
|
||||
>
|
||||
<div class="flex items-center w-full gap-x-3">
|
||||
<div
|
||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||
>
|
||||
<img
|
||||
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||
:src="icons[nav.id]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<p
|
||||
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-medium"
|
||||
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
||||
>
|
||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
|
||||
import { TransitionGroup } from "vue";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
// Style information
|
||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "text-green-500",
|
||||
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-blue-500",
|
||||
[GameStatusEnum.Updating]: "text-blue-500",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-600",
|
||||
};
|
||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Not installed",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading...",
|
||||
[GameStatusEnum.Installed]: "Installed",
|
||||
[GameStatusEnum.Updating]: "Updating...",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||
[GameStatusEnum.Running]: "Running",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
const games: {
|
||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||
} = {};
|
||||
const icons: { [key: string]: string } = {};
|
||||
|
||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) rawGames.value = [];
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||
for (const game of newGames) {
|
||||
if (games[game.id]) continue;
|
||||
games[game.id] = await useGame(game.id);
|
||||
}
|
||||
for (const game of newGames) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
await calculateGames();
|
||||
|
||||
const navigation = computed(() =>
|
||||
rawGames.value.map((game) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type == GameStatusEnum.Installed ||
|
||||
status.value.type == GameStatusEnum.SetupRequired
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
})
|
||||
);
|
||||
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
|
||||
navigation.value
|
||||
);
|
||||
|
||||
const filteredNavigation = computed(() => {
|
||||
if (!searchQuery.value)
|
||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return navigation.value
|
||||
.filter((nav) => nav.label.toLowerCase().includes(query))
|
||||
.map((e, i) => ({ ...e, index: i }));
|
||||
});
|
||||
|
||||
listen("update_library", async (event) => {
|
||||
console.log("Updating library");
|
||||
let oldNavigation = navigation.value[currentNavigation.value];
|
||||
await calculateGames();
|
||||
recalculateNavigation();
|
||||
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
||||
console.log("Triggered");
|
||||
router.push("/library");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
>
|
||||
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
||||
<Wordmark class="mt-1" />
|
||||
3
composables/app-state.ts
Normal file
3
composables/app-state.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { AppState } from "~/types";
|
||||
|
||||
export const useAppState = () => useState<AppState>("state");
|
||||
@@ -32,5 +32,3 @@ listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
|
||||
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
|
||||
@@ -43,7 +43,6 @@ export const useGame = async (gameId: string) => {
|
||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||
|
||||
listen(`update_game/${gameId}`, (event) => {
|
||||
console.log(event);
|
||||
const payload: {
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
@@ -65,13 +65,7 @@ export function setupHooks() {
|
||||
*/
|
||||
}
|
||||
|
||||
export function initialNavigation(state: ReturnType<typeof useAppState>) {
|
||||
if (!state.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "App state not valid",
|
||||
fatal: true,
|
||||
});
|
||||
export function initialNavigation(state: Ref<AppState>) {
|
||||
const router = useRouter();
|
||||
|
||||
switch (state.value.status) {
|
||||
1
drop-base
Submodule
1
drop-base
Submodule
Submodule drop-base added at 26698e5b06
@@ -7,7 +7,6 @@
|
||||
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
|
||||
>
|
||||
<Logo class="h-10 w-auto sm:h-12" />
|
||||
|
||||
</header>
|
||||
<main
|
||||
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
|
||||
Submodule libs/drop-base deleted from 04125e89be
@@ -1,303 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
>
|
||||
<MagnifyingGlassIcon
|
||||
class="h-5 w-5 text-zinc-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="() => calculateGames(true, true)"
|
||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
||||
>
|
||||
<ArrowPathIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||
<Disclosure
|
||||
as="div"
|
||||
v-for="(nav, navIndex) in filteredNavigation"
|
||||
:key="nav.id"
|
||||
class="first:pt-0 last:pb-0"
|
||||
v-slot="{ open }"
|
||||
:default-open="nav.deft"
|
||||
>
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
|
||||
>
|
||||
<span class="text-sm font-semibold font-display">{{
|
||||
nav.name
|
||||
}}</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
|
||||
<NuxtLink
|
||||
v-for="item in nav.items"
|
||||
:key="nav.id"
|
||||
:class="[
|
||||
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||
currentNavigation == item.id
|
||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||
: item.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="item.route"
|
||||
>
|
||||
<div class="flex items-center w-full gap-x-2">
|
||||
<div
|
||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||
>
|
||||
<img
|
||||
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
|
||||
:src="icons[item.id]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
<p
|
||||
class="text-sm whitespace-nowrap font-display font-semibold"
|
||||
>
|
||||
{{ item.label }}
|
||||
</p>
|
||||
<p
|
||||
class="truncate text-[10px] font-bold uppercase font-display"
|
||||
:class="[
|
||||
gameStatusTextStyle[games[item.id].status.value.type],
|
||||
]"
|
||||
>
|
||||
{{ gameStatusText[games[item.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
||||
>
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
MagnifyingGlassIcon,
|
||||
MinusSmallIcon,
|
||||
PlusSmallIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
GameStatusEnum,
|
||||
type Collection as Collection,
|
||||
type Game,
|
||||
type GameStatus,
|
||||
} from "~/types";
|
||||
import { TransitionGroup } from "vue";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
// Style information
|
||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "text-green-500",
|
||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-700",
|
||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||
};
|
||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Not installed",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading...",
|
||||
[GameStatusEnum.Validating]: "Validating...",
|
||||
[GameStatusEnum.Installed]: "Installed",
|
||||
[GameStatusEnum.Updating]: "Updating...",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||
[GameStatusEnum.Running]: "Running",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
const loading = ref(false);
|
||||
const games: {
|
||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||
} = {};
|
||||
const icons: { [key: string]: string } = {};
|
||||
|
||||
const collections: Ref<Collection[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false, forceRefresh = false) {
|
||||
if (clearAll) {
|
||||
collections.value = [];
|
||||
loading.value = true;
|
||||
}
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<Game[]>("fetch_library", {
|
||||
hardRefresh: forceRefresh,
|
||||
});
|
||||
const otherCollections = await invoke<Collection[]>("fetch_collections", {
|
||||
hardRefresh: forceRefresh,
|
||||
});
|
||||
const allGames = [
|
||||
...newGames,
|
||||
...otherCollections
|
||||
.map((e) => e.entries)
|
||||
.flat()
|
||||
.map((e) => e.game),
|
||||
].filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
for (const game of allGames) {
|
||||
if (games[game.id]) continue;
|
||||
games[game.id] = await useGame(game.id);
|
||||
}
|
||||
for (const game of allGames) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
|
||||
const libraryCollection = {
|
||||
id: "library",
|
||||
name: "Library",
|
||||
isDefault: true,
|
||||
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
|
||||
} satisfies Collection;
|
||||
|
||||
loading.value = false;
|
||||
collections.value = [libraryCollection, ...otherCollections];
|
||||
}
|
||||
|
||||
// Wait up to 300 ms for the library to load, otherwise
|
||||
// show the loading state while we while
|
||||
await new Promise<void>((r) => {
|
||||
let hasResolved = false;
|
||||
const resolveFunc = () => {
|
||||
if (!hasResolved) r();
|
||||
hasResolved = true;
|
||||
};
|
||||
calculateGames(true).then(resolveFunc);
|
||||
setTimeout(resolveFunc, 300);
|
||||
});
|
||||
|
||||
const navigation = computed(() =>
|
||||
collections.value.map((collection) => {
|
||||
const items = collection.entries.map(({ game }) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() => status.value.type != GameStatusEnum.Remote
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
||||
return {
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
deft: collection.isDefault,
|
||||
items,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const route = useRoute();
|
||||
const currentNavigation = computed(() => {
|
||||
return route.path.slice("/library/".length);
|
||||
});
|
||||
|
||||
const filteredNavigation = computed(() => {
|
||||
if (!searchQuery.value)
|
||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return navigation.value
|
||||
.map((c) => ({
|
||||
...c,
|
||||
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
|
||||
}))
|
||||
.filter((e) => e.items.length > 0);
|
||||
});
|
||||
|
||||
listen("update_library", async (event) => {
|
||||
console.log("Updating library");
|
||||
let oldNavigation = currentNavigation.value;
|
||||
await calculateGames();
|
||||
if (oldNavigation !== currentNavigation.value) {
|
||||
router.push("/library");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { AppState } from "~/types";
|
||||
|
||||
export const useAppState = () => useState<AppState | undefined>("state");
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "view",
|
||||
"private": true,
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt generate",
|
||||
"dev": "nuxt dev",
|
||||
"postinstall": "nuxt prepare",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"koa": "^2.16.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.16.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-full w-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Device authorization
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-md mx-auto">
|
||||
Open Drop on another one of your devices, and use your account
|
||||
dropdown to "Authorize client", and enter the code below.
|
||||
</p>
|
||||
<div
|
||||
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
|
||||
>
|
||||
<span v-for="letter in code.split('')">{{ letter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
|
||||
><span aria-hidden="true">←</span> Use a different method
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const code = await invoke<string>("auth_initiate_code");
|
||||
|
||||
definePageMeta({
|
||||
layout: "mini",
|
||||
});
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div class="grow w-full h-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<WrenchScrewdriverIcon
|
||||
class="h-12 w-12 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Under construction
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-lg">
|
||||
This page hasn't been implemented yet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
WrenchScrewdriverIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div class="grow w-full h-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<WrenchScrewdriverIcon
|
||||
class="h-12 w-12 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Under construction
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-lg">
|
||||
This page hasn't been implemented yet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
WrenchScrewdriverIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
</script>
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="grow w-full h-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<WrenchScrewdriverIcon
|
||||
class="h-12 w-12 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Under construction
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-lg">
|
||||
This page hasn't been implemented yet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { WrenchScrewdriverIcon } from "@heroicons/vue/20/solid";
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"exclude": ["src-tauri/**/*"]
|
||||
}
|
||||
8091
main/yarn.lock
8091
main/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,5 @@ export default defineNuxtConfig({
|
||||
|
||||
ssr: false,
|
||||
|
||||
extends: [["../libs/drop-base"]],
|
||||
|
||||
app: {
|
||||
baseURL: "/main",
|
||||
}
|
||||
extends: [["./drop-base"]],
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
## This script is largely useless, because there's not much we can do about AppImage size
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# build tauri apps
|
||||
# NO_STRIP=true yarn tauri build -- --verbose
|
||||
|
||||
# unpack appimage
|
||||
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)
|
||||
"$APPIMAGE" --appimage-extract
|
||||
|
||||
# strip binary
|
||||
APPIMAGE_UNPACK="./squashfs-root"
|
||||
find $APPIMAGE_UNPACK -type f -exec strip -s {} \;
|
||||
|
||||
APPIMAGETOOL=$(echo "obsolete-appimagetool-$ARCH.AppImage")
|
||||
wget -O $APPIMAGETOOL "https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGETOOL"
|
||||
chmod +x $APPIMAGETOOL
|
||||
|
||||
APPIMAGE_OUTPUT=$(./$APPIMAGETOOL $APPIMAGE_UNPACK | grep ".AppImage" | grep squashfs-root | awk '{ print $6 }')
|
||||
|
||||
mv $APPIMAGE_OUTPUT "$APPIMAGE"
|
||||
44
package.json
44
package.json
@@ -1,22 +1,46 @@
|
||||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.3.0-rc-8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node ./build.mjs",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.1.1"
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"koa": "^2.16.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.16.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.7.1"
|
||||
}
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tauri-apps/cli": ">=2.0.0",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@
|
||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
:key="index"
|
||||
:key="url"
|
||||
:src="url"
|
||||
loading="lazy"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
v-show="index === currentImageIndex"
|
||||
/>
|
||||
@@ -157,8 +158,8 @@
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Install {{ game.mName }}?
|
||||
<h3 class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
@@ -243,10 +244,7 @@
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="versionOptions === null || versionOptions?.length == 0"
|
||||
class="mt-1 rounded-md bg-red-600/10 p-4"
|
||||
>
|
||||
<div v-else class="mt-1 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" />
|
||||
@@ -259,27 +257,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full flex items-center justify-center p-4">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-7 h-7 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
@@ -374,7 +351,9 @@
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="!(versionOptions && versionOptions.length > 0)"
|
||||
:disabled="
|
||||
!(versionOptions && versionOptions.length > 0)
|
||||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
@@ -392,18 +371,7 @@
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
|
||||
<!--
|
||||
Dear future DecDuck,
|
||||
This v-if is necessary for Vue rendering reasons
|
||||
(it tries to access the game version for not installed games)
|
||||
You have already tried to remove it
|
||||
Don't.
|
||||
-->
|
||||
<GameOptionsModal
|
||||
v-if="status.type === GameStatusEnum.Installed"
|
||||
v-model="configureModalOpen"
|
||||
:game-id="game.id"
|
||||
/>
|
||||
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
|
||||
|
||||
<Transition
|
||||
enter="transition ease-out duration-300"
|
||||
@@ -453,7 +421,7 @@
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
v-show="currentImageIndex === index"
|
||||
:key="index"
|
||||
:key="url"
|
||||
:src="url"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||
@@ -474,6 +442,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
@@ -511,10 +483,7 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||
|
||||
// Get all available images
|
||||
const mediaUrls = await Promise.all(
|
||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||
const src = await useObject(v);
|
||||
return src;
|
||||
})
|
||||
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
||||
);
|
||||
|
||||
const htmlDescription = micromark(game.value.mDescription);
|
||||
@@ -528,19 +497,20 @@ const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
installDirs.value = undefined;
|
||||
|
||||
try {
|
||||
versionOptions.value = await invoke("fetch_game_version_options", {
|
||||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
console.log(versionOptions.value);
|
||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
versionOptions.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,11 +536,12 @@ async function install() {
|
||||
}
|
||||
|
||||
async function resumeDownload() {
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id })
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
@@ -4,18 +4,18 @@
|
||||
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
||||
>
|
||||
<div
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
|
||||
>
|
||||
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
|
||||
<span v-if="stats.time > 0" class="text-xs text-zinc-400"
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||
<span v-if="stats.time > 0" class="text-sm"
|
||||
>{{ formatTime(stats.time) }} left</span
|
||||
>
|
||||
</div>
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||
<div
|
||||
v-for="bar in speedHistory"
|
||||
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||
class="w-[3px] bg-blue-600 rounded-t-full"
|
||||
class="w-[8px] bg-blue-600/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,9 +62,9 @@
|
||||
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
||||
><span class="text-zinc-300">{{
|
||||
formatKilobytes(element.current / 1000)
|
||||
}}B</span>
|
||||
}}</span>
|
||||
/
|
||||
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
|
||||
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
|
||||
><ServerIcon class="size-5"
|
||||
/></span>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||
|
||||
// const actionNames = {
|
||||
// [GameStatusEnum.Downloading]: "downloading",
|
||||
@@ -105,12 +105,12 @@ window.addEventListener("resize", (event) => {
|
||||
|
||||
const queue = useQueueState();
|
||||
const stats = useStatsState();
|
||||
const speedHistory = useDownloadHistory();
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 4);
|
||||
const speedHistory = useState<Array<number>>(() => []);
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||
const speedMax = computed(
|
||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
|
||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
|
||||
);
|
||||
const previousGameId = useState<string | undefined>('previous_game');
|
||||
const previousGameId = ref<string | undefined>();
|
||||
|
||||
const games: Ref<{
|
||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||
@@ -122,15 +122,14 @@ function resetHistoryGraph() {
|
||||
}
|
||||
function checkReset(v: QueueState) {
|
||||
const currentGame = v.queue.at(0)?.meta.id;
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
|
||||
// If we're finished
|
||||
if (!currentGame && previousGameId.value) {
|
||||
previousGameId.value = undefined;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
// If we started a new download
|
||||
if (currentGame && !previousGameId.value) {
|
||||
previousGameId.value = currentGame;
|
||||
@@ -150,10 +149,9 @@ watch(queue, (v) => {
|
||||
});
|
||||
|
||||
watch(stats, (v) => {
|
||||
if(v.speed == 0) return;
|
||||
const newLength = speedHistory.value.push(v.speed);
|
||||
if (newLength > speedHistoryMax.value) {
|
||||
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
|
||||
speedHistory.value.splice(0, 1);
|
||||
}
|
||||
checkReset(queue.value);
|
||||
});
|
||||
@@ -185,7 +183,7 @@ async function cancelGame(meta: DownloadableMetadata) {
|
||||
}
|
||||
|
||||
function formatKilobytes(bytes: number): string {
|
||||
const units = ["K", "M", "G", "T", "P"];
|
||||
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
const scalar = 1000;
|
||||
7
pages/settings/interface.vue
Normal file
7
pages/settings/interface.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
2255
src-tauri/Cargo.lock
generated
2255
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,101 +1,117 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.3"
|
||||
# authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
version = "0.3.0-rc-8"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["drop-consts",
|
||||
"drop-database",
|
||||
"drop-downloads",
|
||||
"drop-errors", "drop-library",
|
||||
"drop-native-library",
|
||||
"drop-process",
|
||||
"drop-remote",
|
||||
]
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib", "staticlib"]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "drop_app_lib"
|
||||
# rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
boxcar = "0.2.7"
|
||||
dirs = "6.0.0"
|
||||
drop-database = { path = "./drop-database" }
|
||||
drop-downloads = { path = "./drop-downloads" }
|
||||
drop-errors = { path = "./drop-errors" }
|
||||
drop-native-library = { path = "./drop-native-library" }
|
||||
drop-process = { path = "./drop-process" }
|
||||
drop-remote = { path = "./drop-remote" }
|
||||
futures-lite = "2.6.0"
|
||||
hex = "0.4.3"
|
||||
http = "1.1.0"
|
||||
known-folders = "1.2.0"
|
||||
log = "0.4.22"
|
||||
md5 = "0.7.0"
|
||||
rayon = "1.10.0"
|
||||
regex = "1.11.1"
|
||||
reqwest-websocket = "0.5.0"
|
||||
serde_json = "1"
|
||||
tar = "0.4.44"
|
||||
tauri = { version = "2.7.0", features = ["protocol-asset", "tray-icon"] }
|
||||
tauri-plugin-autostart = "2.0.0"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tempfile = "3.19.1"
|
||||
url = "2.5.2"
|
||||
serde_json = "1"
|
||||
webbrowser = "1.0.2"
|
||||
whoami = "1.6.0"
|
||||
url = "2.5.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
log = "0.4.22"
|
||||
hex = "0.4.3"
|
||||
tauri-plugin-dialog = "2"
|
||||
http = "1.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
md5 = "0.7.0"
|
||||
chrono = "0.4.38"
|
||||
tauri-plugin-os = "2"
|
||||
boxcar = "0.2.7"
|
||||
umu-wrapper-lib = "0.1.0"
|
||||
tauri-plugin-autostart = "2.0.0"
|
||||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
slice-deque = "0.3.0"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
atomic-instant-full = "0.1.0"
|
||||
cacache = "13.1.0"
|
||||
http-serde = "2.1.1"
|
||||
reqwest-middleware = "0.4.0"
|
||||
reqwest-middleware-cache = "0.1.1"
|
||||
deranged = "=0.4.0"
|
||||
droplet-rs = "0.7.3"
|
||||
gethostname = "1.0.1"
|
||||
zstd = "0.13.3"
|
||||
tar = "0.4.44"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
tempfile = "3.19.1"
|
||||
schemars = "0.8.22"
|
||||
sha1 = "0.10.6"
|
||||
dirs = "6.0.0"
|
||||
whoami = "1.6.0"
|
||||
filetime = "0.2.25"
|
||||
walkdir = "2.5.0"
|
||||
known-folders = "1.2.0"
|
||||
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
bitcode = "0.6.6"
|
||||
async-trait = "0.1.88"
|
||||
futures = "0.3.31"
|
||||
tokio-util = { version = "0.7.15", features = ["io"] }
|
||||
async-scoped = { version = "0.9.0", features = ["use-tokio"] }
|
||||
async-once-cell = "0.5.4"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
version = "0.1.5"
|
||||
features = ["curly"]
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.40.0"
|
||||
features = ["rt", "tokio-macros", "signal"]
|
||||
|
||||
[dependencies.log4rs]
|
||||
version = "1.3.0"
|
||||
features = ["console_appender", "file_appender"]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.22"
|
||||
default-features = false
|
||||
features = [
|
||||
"blocking",
|
||||
"http2",
|
||||
"json",
|
||||
"native-tls-alpn",
|
||||
"rustls-tls",
|
||||
"rustls-tls-native-roots",
|
||||
"stream",
|
||||
]
|
||||
|
||||
[dependencies.rustix]
|
||||
version = "0.38.37"
|
||||
features = ["fs"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.10.0"
|
||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||
|
||||
[dependencies.dropbreak]
|
||||
git = "https://github.com/Drop-OSS/dropbreak.git"
|
||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
default-features = false
|
||||
features = ["json", "http2", "rustls-tls-webpki-roots", "stream"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive", "rc"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.10.0"
|
||||
features = ["fast-rng", "macro-diagnostics", "v4"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
|
||||
panic = 'abort'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "drop-consts"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
@@ -1,15 +0,0 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static DATA_ROOT_PREFIX: &'static str = "drop";
|
||||
#[cfg(debug_assertions)]
|
||||
static DATA_ROOT_PREFIX: &str = "drop-debug";
|
||||
|
||||
pub static DATA_ROOT_DIR: LazyLock<&'static PathBuf> =
|
||||
LazyLock::new(|| Box::leak(Box::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX))));
|
||||
|
||||
pub static CACHE_DIR: LazyLock<&'static PathBuf> =
|
||||
LazyLock::new(|| Box::leak(Box::new(DATA_ROOT_DIR.join("cache"))));
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "drop-database"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitcode = "0.6.7"
|
||||
chrono = "0.4.42"
|
||||
drop-consts = { path = "../drop-consts" }
|
||||
drop-library = { path = "../drop-library" }
|
||||
drop-native-library = { path = "../drop-native-library" }
|
||||
log = "0.4.28"
|
||||
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
|
||||
"rmp_serde_1_3",
|
||||
] }
|
||||
rustbreak = "2.0.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_with = "3.14.0"
|
||||
url = "2.5.7"
|
||||
whoami = "1.6.1"
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
use std::{
|
||||
fs::{self, create_dir_all},
|
||||
mem::ManuallyDrop,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use drop_consts::DATA_ROOT_DIR;
|
||||
use log::{debug, error, info, warn};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::DB;
|
||||
|
||||
use super::models::data::Database;
|
||||
|
||||
// Custom JSON serializer to support everything we need
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DropDatabaseSerializer;
|
||||
|
||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||
for DropDatabaseSerializer
|
||||
{
|
||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
||||
}
|
||||
|
||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||
let mut buf = Vec::new();
|
||||
s.read_to_end(&mut buf)
|
||||
.map_err(|e| rustbreak::error::DeSerError::Internal(e.to_string()))?;
|
||||
let (val, _version) =
|
||||
native_model::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
pub type DatabaseInterface =
|
||||
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||
|
||||
pub trait DatabaseImpls {
|
||||
fn set_up_database() -> DatabaseInterface;
|
||||
}
|
||||
impl DatabaseImpls for DatabaseInterface {
|
||||
fn set_up_database() -> DatabaseInterface {
|
||||
let db_path = DATA_ROOT_DIR.join("drop.db");
|
||||
let games_base_dir = DATA_ROOT_DIR.join("games");
|
||||
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
||||
let cache_dir = DATA_ROOT_DIR.join("cache");
|
||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
||||
|
||||
debug!("creating data directory at {DATA_ROOT_DIR:?}");
|
||||
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap();
|
||||
create_dir_all(&games_base_dir).unwrap();
|
||||
create_dir_all(&logs_root_dir).unwrap();
|
||||
create_dir_all(&cache_dir).unwrap();
|
||||
create_dir_all(&pfx_dir).unwrap();
|
||||
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
if exists {
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||
}
|
||||
} else {
|
||||
let default = Database::new(games_base_dir, None);
|
||||
debug!(
|
||||
"Creating database at path {}",
|
||||
db_path.as_os_str().to_str().unwrap()
|
||||
);
|
||||
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||
fn handle_invalid_database(
|
||||
_e: RustbreakError,
|
||||
db_path: PathBuf,
|
||||
games_base_dir: PathBuf,
|
||||
cache_dir: PathBuf,
|
||||
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
warn!("{_e}");
|
||||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
let mut base = db_path.clone();
|
||||
base.set_file_name(format!("drop.db.backup-{time}"));
|
||||
base
|
||||
};
|
||||
info!("old database stored at: {}", new_path.to_string_lossy());
|
||||
fs::rename(&db_path, &new_path).unwrap();
|
||||
|
||||
let db = Database::new(
|
||||
games_base_dir.into_os_string().into_string().unwrap(),
|
||||
Some(new_path),
|
||||
);
|
||||
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
|
||||
// To automatically save the database upon drop
|
||||
pub struct DBRead<'a>(pub(crate) RwLockReadGuard<'a, Database>);
|
||||
pub struct DBWrite<'a>(pub(crate) ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||
impl<'a> Deref for DBWrite<'a> {
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> DerefMut for DBWrite<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl Drop for DBWrite<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.0);
|
||||
}
|
||||
|
||||
match DB.save() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
error!("database failed to save with error {e}");
|
||||
panic!("database failed to save with error {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use native_model::{Decode, Encode};
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
|
||||
pub static DROP_DATA_PATH: &str = ".dropdata";
|
||||
|
||||
mod v1 {
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
pub struct DropData {
|
||||
pub game_id: String,
|
||||
pub game_version: String,
|
||||
pub contexts: Mutex<HashMap<String, bool>>,
|
||||
pub base_path: PathBuf,
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
Self {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
contexts: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
match DropData::read(&base_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => DropData::new(game_id, game_version, base_path),
|
||||
}
|
||||
}
|
||||
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
|
||||
let mut file = File::open(base_path.join(DROP_DATA_PATH))?;
|
||||
|
||||
let mut s = Vec::new();
|
||||
file.read_to_end(&mut s)?;
|
||||
|
||||
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match file.write_all(&manifest_raw) {
|
||||
Ok(()) => {}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
}
|
||||
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
|
||||
*self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
|
||||
}
|
||||
pub fn set_context(&self, context: String, state: bool) {
|
||||
self.contexts.lock().unwrap().entry(context).insert_entry(state);
|
||||
}
|
||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||
self.contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use std::{mem::ManuallyDrop, sync::LazyLock};
|
||||
|
||||
use log::error;
|
||||
|
||||
use crate::db::{DBRead, DBWrite, DatabaseImpls, DatabaseInterface};
|
||||
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod models;
|
||||
pub mod process;
|
||||
pub mod runtime_models;
|
||||
pub mod drop_data;
|
||||
|
||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => DBRead(data),
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {e}");
|
||||
panic!("database borrow failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {e}");
|
||||
panic!("database borrow mut failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Linux,
|
||||
MacOs,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const HOST: Platform = Self::Windows;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const HOST: Platform = Self::MacOs;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const HOST: Platform = Self::Linux;
|
||||
|
||||
pub fn is_case_sensitive(&self) -> bool {
|
||||
match self {
|
||||
Self::Windows | Self::MacOs => false,
|
||||
Self::Linux => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Platform {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().trim() {
|
||||
"windows" => Self::Windows,
|
||||
"linux" => Self::Linux,
|
||||
"mac" | "macos" => Self::MacOs,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<whoami::Platform> for Platform {
|
||||
fn from(value: whoami::Platform) -> Self {
|
||||
match value {
|
||||
whoami::Platform::Windows => Platform::Windows,
|
||||
whoami::Platform::Linux => Platform::Linux,
|
||||
whoami::Platform::MacOS => Platform::MacOs,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Game {
|
||||
pub id: String,
|
||||
m_name: String,
|
||||
m_short_description: String,
|
||||
m_description: String,
|
||||
// mDevelopers
|
||||
// mPublishers
|
||||
m_icon_object_id: String,
|
||||
m_banner_object_id: String,
|
||||
m_cover_object_id: String,
|
||||
m_image_library_object_ids: Vec<String>,
|
||||
m_image_carousel_object_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
id: String,
|
||||
username: String,
|
||||
admin: bool,
|
||||
display_name: String,
|
||||
profile_picture_object_id: String,
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "drop-downloads"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
atomic-instant-full = "0.1.0"
|
||||
drop-database = { path = "../drop-database" }
|
||||
drop-errors = { path = "../drop-errors" }
|
||||
# can't depend, cycle
|
||||
# drop-native-library = { path = "../drop-native-library" }
|
||||
log = "0.4.22"
|
||||
parking_lot = "0.12.4"
|
||||
serde = "1.0.219"
|
||||
tauri = { version = "2.7.0" }
|
||||
throttle_my_fn = "0.2.6"
|
||||
@@ -1,362 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
mpsc::{Receiver, Sender, channel},
|
||||
},
|
||||
thread::{JoinHandle, spawn},
|
||||
};
|
||||
|
||||
use drop_database::models::data::DownloadableMetadata;
|
||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
||||
use log::{debug, error, info, warn};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::{
|
||||
download_manager_frontend::DownloadStatus, events::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}
|
||||
};
|
||||
|
||||
use super::{
|
||||
download_manager_frontend::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||
downloadable::Downloadable,
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::ProgressObject,
|
||||
queue::Queue,
|
||||
},
|
||||
};
|
||||
|
||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
||||
|
||||
pub struct DownloadManagerBuilder {
|
||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||
download_queue: Queue,
|
||||
command_receiver: Receiver<DownloadManagerSignal>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
progress: CurrentProgressObject,
|
||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||
app_handle: AppHandle,
|
||||
|
||||
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||
active_control_flag: Option<DownloadThreadControl>,
|
||||
}
|
||||
impl DownloadManagerBuilder {
|
||||
pub fn build(app_handle: AppHandle) -> DownloadManager {
|
||||
let queue = Queue::new();
|
||||
let (command_sender, command_receiver) = channel();
|
||||
let active_progress = Arc::new(Mutex::new(None));
|
||||
let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
|
||||
|
||||
let manager = Self {
|
||||
download_agent_registry: HashMap::new(),
|
||||
download_queue: queue.clone(),
|
||||
command_receiver,
|
||||
status: status.clone(),
|
||||
sender: command_sender.clone(),
|
||||
progress: active_progress.clone(),
|
||||
app_handle,
|
||||
|
||||
current_download_thread: Mutex::new(None),
|
||||
active_control_flag: None,
|
||||
};
|
||||
|
||||
let terminator = spawn(|| manager.manage_queue());
|
||||
|
||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||
}
|
||||
|
||||
fn set_status(&self, status: DownloadManagerStatus) {
|
||||
*self.status.lock().unwrap() = status;
|
||||
}
|
||||
|
||||
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||
self.download_queue.pop_front();
|
||||
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
||||
self.cleanup_current_download();
|
||||
download_agent
|
||||
}
|
||||
|
||||
// CAREFUL WITH THIS FUNCTION
|
||||
// Make sure the download thread is terminated
|
||||
fn cleanup_current_download(&mut self) {
|
||||
self.active_control_flag = None;
|
||||
*self.progress.lock().unwrap() = None;
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
|
||||
if let Some(unfinished_thread) = download_thread_lock.take()
|
||||
&& !unfinished_thread.is_finished()
|
||||
{
|
||||
unfinished_thread.join().unwrap();
|
||||
}
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
fn stop_and_wait_current_download(&self) -> bool {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(current_flag) = &self.active_control_flag {
|
||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||
return current_download_thread.join().is_ok();
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
loop {
|
||||
let signal = match self.command_receiver.recv() {
|
||||
Ok(signal) => signal,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
match signal {
|
||||
DownloadManagerSignal::Go => {
|
||||
self.manage_go_signal();
|
||||
}
|
||||
DownloadManagerSignal::Stop => {
|
||||
self.manage_stop_signal();
|
||||
}
|
||||
DownloadManagerSignal::Completed(meta) => {
|
||||
self.manage_completed_signal(meta);
|
||||
}
|
||||
DownloadManagerSignal::Queue(download_agent) => {
|
||||
self.manage_queue_signal(download_agent);
|
||||
}
|
||||
DownloadManagerSignal::Error(e) => {
|
||||
self.manage_error_signal(e);
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIQueue => {
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||
self.push_ui_stats_update(kbs, time);
|
||||
}
|
||||
DownloadManagerSignal::Finish => {
|
||||
self.stop_and_wait_current_download();
|
||||
return Ok(());
|
||||
}
|
||||
DownloadManagerSignal::Cancel(meta) => {
|
||||
self.manage_cancel_signal(&meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
debug!("got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
debug!("queue metadata: {meta:?}");
|
||||
|
||||
if self.download_queue.exists(meta.clone()) {
|
||||
warn!("download with same ID already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
download_agent.on_queued(&self.app_handle);
|
||||
self.download_queue.append(meta.clone());
|
||||
self.download_agent_registry.insert(meta, download_agent);
|
||||
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
debug!("got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
debug!(
|
||||
"Download agent registry: {:?}",
|
||||
self.download_agent_registry.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
|
||||
let agent_data = if let Some(agent_data) = self.download_queue.read().front() {
|
||||
agent_data.clone()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let download_agent = self
|
||||
.download_agent_registry
|
||||
.get(&agent_data)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let status = download_agent.status();
|
||||
|
||||
// This download is already going
|
||||
if status != DownloadStatus::Queued {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all others are marked as queued
|
||||
for agent in self.download_agent_registry.values() {
|
||||
if agent.metadata() != agent_data && agent.status() != DownloadStatus::Queued {
|
||||
agent.on_queued(&self.app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
info!("starting download for {agent_data:?}");
|
||||
self.active_control_flag = Some(download_agent.control_flag());
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
let app_handle = self.app_handle.clone();
|
||||
|
||||
*download_thread_lock = Some(spawn(move || {
|
||||
loop {
|
||||
let download_result = match download_agent.download(&app_handle) {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// If the download gets canceled
|
||||
// immediately return, on_cancelled gets called for us earlier
|
||||
if !download_result {
|
||||
return;
|
||||
}
|
||||
|
||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
||||
return;
|
||||
}
|
||||
|
||||
let validate_result = match download_agent.validate(&app_handle) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"download {:?} has validation error {}",
|
||||
download_agent.metadata(),
|
||||
&e
|
||||
);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
||||
return;
|
||||
}
|
||||
|
||||
if validate_result {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
self.set_status(DownloadManagerStatus::Downloading);
|
||||
let active_control_flag = self.active_control_flag.clone().unwrap();
|
||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
}
|
||||
fn manage_stop_signal(&mut self) {
|
||||
debug!("got signal Stop");
|
||||
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
}
|
||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
debug!("got signal Completed");
|
||||
if let Some(interface) = self.download_queue.read().front()
|
||||
&& interface == &meta
|
||||
{
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
|
||||
self.push_ui_queue_update();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
debug!("got signal Error");
|
||||
if let Some(metadata) = self.download_queue.read().front()
|
||||
&& let Some(current_agent) = self.download_agent_registry.get(metadata)
|
||||
{
|
||||
current_agent.on_error(&self.app_handle, &error);
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(metadata);
|
||||
}
|
||||
self.push_ui_queue_update();
|
||||
self.set_status(DownloadManagerStatus::Error);
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
debug!("got signal Cancel");
|
||||
|
||||
// If the current download is the one we're tryna cancel
|
||||
if let Some(current_metadata) = self.download_queue.read().front()
|
||||
&& current_metadata == meta
|
||||
&& let Some(current_download) = self.download_agent_registry.get(current_metadata)
|
||||
{
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
current_download.on_cancelled(&self.app_handle);
|
||||
self.stop_and_wait_current_download();
|
||||
|
||||
self.download_queue.pop_front();
|
||||
|
||||
self.cleanup_current_download();
|
||||
self.download_agent_registry.remove(meta);
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
}
|
||||
// else just cancel it
|
||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
debug!(
|
||||
"removed {:?} from queue {:?}",
|
||||
removed.map(|x| x.metadata()),
|
||||
self.download_queue.read()
|
||||
);
|
||||
}
|
||||
}
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||
|
||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||
}
|
||||
fn push_ui_queue_update(&self) {
|
||||
let queue = &self.download_queue.read();
|
||||
let queue_objs = queue
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let val = self.download_agent_registry.get(key).unwrap();
|
||||
QueueUpdateEventQueueData {
|
||||
meta: DownloadableMetadata::clone(key),
|
||||
status: val.status(),
|
||||
progress: val.progress().get_progress(),
|
||||
current: val.progress().sum(),
|
||||
max: val.progress().get_max(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use drop_database::models::data::DownloadableMetadata;
|
||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use super::{
|
||||
download_manager_frontend::DownloadStatus,
|
||||
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloadables are responsible for managing their specific object's download state
|
||||
* e.g, the GameDownloadAgent is responsible for pushing game updates
|
||||
*
|
||||
* But the download manager manages the queue state
|
||||
*/
|
||||
pub trait Downloadable: Send + Sync {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject>;
|
||||
fn control_flag(&self) -> DownloadThreadControl;
|
||||
fn status(&self) -> DownloadStatus;
|
||||
fn metadata(&self) -> DownloadableMetadata;
|
||||
fn on_queued(&self, app_handle: &AppHandle);
|
||||
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||
fn on_complete(&self, app_handle: &AppHandle);
|
||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user