mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 00:02:41 +10:00
Compare commits
22 Commits
async
...
52-feature
| Author | SHA1 | Date | |
|---|---|---|---|
| 96df57ac54 | |||
| 8069616f2b | |||
| 5eef2bf60f | |||
| ec6294b8e7 | |||
| 17c375bcab | |||
| cb55ac2bf5 | |||
| e11db851a5 | |||
| 16365713cf | |||
| 3b830e2a44 | |||
| 75a4b73ee1 | |||
| 339d707092 | |||
| 776dc8fe7a | |||
| dbe8c8df4d | |||
| 35f49b8811 | |||
| cc5339a389 | |||
| 6104bfda72 | |||
| be688cb18f | |||
| 13cc69f10e | |||
| 574782f445 | |||
| b5a8543194 | |||
| d0e4aea5ce | |||
| 739e6166c5 |
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@ -51,15 +51,50 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
# 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
|
- name: install frontend dependencies
|
||||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
with:
|
||||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||||
releaseName: 'Auto-release v__VERSION__'
|
releaseName: 'Auto-release v__VERSION__'
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,3 +27,6 @@ dist-ssr
|
|||||||
|
|
||||||
src-tauri/flamegraph.svg
|
src-tauri/flamegraph.svg
|
||||||
src-tauri/perf*
|
src-tauri/perf*
|
||||||
|
|
||||||
|
/*.AppImage
|
||||||
|
/squashfs-root
|
||||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@ -1,9 +1,6 @@
|
|||||||
[submodule "drop-base"]
|
|
||||||
path = drop-base
|
|
||||||
url = https://github.com/drop-oss/drop-base
|
|
||||||
[submodule "src-tauri/tailscale/libtailscale"]
|
[submodule "src-tauri/tailscale/libtailscale"]
|
||||||
path = src-tauri/tailscale/libtailscale
|
path = src-tauri/tailscale/libtailscale
|
||||||
url = https://github.com/tailscale/libtailscale.git
|
url = https://github.com/tailscale/libtailscale.git
|
||||||
[submodule "src-tauri/umu/umu-launcher"]
|
[submodule "libs/drop-base"]
|
||||||
path = src-tauri/umu/umu-launcher
|
path = libs/drop-base
|
||||||
url = https://github.com/Open-Wine-Components/umu-launcher.git
|
url = https://github.com/drop-oss/drop-base.git
|
||||||
|
|||||||
48
build.mjs
Normal file
48
build.mjs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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 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,3 +0,0 @@
|
|||||||
import type { AppState } from "~/types";
|
|
||||||
|
|
||||||
export const useAppState = () => useState<AppState>("state");
|
|
||||||
Submodule drop-base deleted from 26698e5b06
1
libs/drop-base
Submodule
1
libs/drop-base
Submodule
Submodule libs/drop-base added at 04125e89be
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<LoadingIndicator />
|
||||||
<NuxtLayout class="select-none w-screen h-screen">
|
<NuxtLayout class="select-none w-screen h-screen">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<ModalStack />
|
<ModalStack />
|
||||||
@ -9,8 +10,6 @@
|
|||||||
import "~/composables/downloads.js";
|
import "~/composables/downloads.js";
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
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 { useAppState } from "./composables/app-state.js";
|
||||||
import {
|
import {
|
||||||
initialNavigation,
|
initialNavigation,
|
||||||
@ -20,19 +19,26 @@ import {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const state = useAppState();
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
router.beforeEach(async () => {
|
async function fetchState() {
|
||||||
try {
|
try {
|
||||||
state.value = JSON.parse(await invoke("fetch_state"));
|
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) {
|
} catch (e) {
|
||||||
console.error("failed to parse state", 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();
|
setupHooks();
|
||||||
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 6.5 MiB |
@ -1,49 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||||
<div class="inline-flex divide-x divide-zinc-900">
|
<div class="inline-flex divide-x divide-zinc-900">
|
||||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="() => buttonActions[props.status.type]()"
|
||||||
|
:class="[
|
||||||
styles[props.status.type],
|
styles[props.status.type],
|
||||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
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',
|
'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" />
|
>
|
||||||
|
<component
|
||||||
|
:is="buttonIcons[props.status.type]"
|
||||||
|
class="-mr-0.5 size-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ buttonNames[props.status.type] }}
|
{{ buttonNames[props.status.type] }}
|
||||||
</button>
|
</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">
|
<div class="h-full">
|
||||||
<MenuButton :class="[
|
<MenuButton
|
||||||
|
:class="[
|
||||||
styles[props.status.type],
|
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',
|
'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',
|
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
<transition
|
||||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
enter-active-class="transition ease-out duration-100"
|
||||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
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
|
<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">
|
<div class="py-1">
|
||||||
<MenuItem v-slot="{ active }">
|
<MenuItem v-if="showOptions" v-slot="{ active }">
|
||||||
<button @click="() => emit('options')" :class="[
|
<button
|
||||||
|
@click="() => emit('options')"
|
||||||
|
:class="[
|
||||||
active
|
active
|
||||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||||
: 'text-zinc-400',
|
: 'text-zinc-400',
|
||||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
Options
|
Options
|
||||||
<Cog6ToothIcon class="size-5" />
|
<Cog6ToothIcon class="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<button @click="() => emit('uninstall')" :class="[
|
<button
|
||||||
|
@click="() => emit('uninstall')"
|
||||||
|
:class="[
|
||||||
active
|
active
|
||||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||||
: 'text-zinc-400',
|
: 'text-zinc-400',
|
||||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
Uninstall
|
Uninstall
|
||||||
<TrashIcon class="size-5" />
|
<TrashIcon class="size-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -61,6 +87,7 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
QueueListIcon,
|
QueueListIcon,
|
||||||
|
ServerIcon,
|
||||||
StopIcon,
|
StopIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "@heroicons/vue/20/solid";
|
} from "@heroicons/vue/20/solid";
|
||||||
@ -78,7 +105,7 @@ const emit = defineEmits<{
|
|||||||
(e: "uninstall"): void;
|
(e: "uninstall"): void;
|
||||||
(e: "kill"): void;
|
(e: "kill"): void;
|
||||||
(e: "options"): void;
|
(e: "options"): void;
|
||||||
(e: "resume"): void
|
(e: "resume"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showDropdown = computed(
|
const showDropdown = computed(
|
||||||
@ -88,6 +115,10 @@ const showDropdown = computed(
|
|||||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
props.status.type === GameStatusEnum.PartiallyInstalled
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showOptions = computed(
|
||||||
|
() => props.status.type === GameStatusEnum.Installed
|
||||||
|
);
|
||||||
|
|
||||||
const styles: { [key in GameStatusEnum]: string } = {
|
const styles: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Remote]:
|
[GameStatusEnum.Remote]:
|
||||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||||
@ -95,6 +126,8 @@ const styles: { [key in GameStatusEnum]: string } = {
|
|||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.Downloading]:
|
[GameStatusEnum.Downloading]:
|
||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"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]:
|
[GameStatusEnum.SetupRequired]:
|
||||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
||||||
[GameStatusEnum.Installed]:
|
[GameStatusEnum.Installed]:
|
||||||
@ -106,42 +139,45 @@ const styles: { [key in GameStatusEnum]: string } = {
|
|||||||
[GameStatusEnum.Running]:
|
[GameStatusEnum.Running]:
|
||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.PartiallyInstalled]:
|
[GameStatusEnum.PartiallyInstalled]:
|
||||||
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Remote]: "Install",
|
[GameStatusEnum.Remote]: "Install",
|
||||||
[GameStatusEnum.Queued]: "Queued",
|
[GameStatusEnum.Queued]: "Queued",
|
||||||
[GameStatusEnum.Downloading]: "Downloading",
|
[GameStatusEnum.Downloading]: "Downloading",
|
||||||
|
[GameStatusEnum.Validating]: "Validating",
|
||||||
[GameStatusEnum.SetupRequired]: "Setup",
|
[GameStatusEnum.SetupRequired]: "Setup",
|
||||||
[GameStatusEnum.Installed]: "Play",
|
[GameStatusEnum.Installed]: "Play",
|
||||||
[GameStatusEnum.Updating]: "Updating",
|
[GameStatusEnum.Updating]: "Updating",
|
||||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||||
[GameStatusEnum.Running]: "Stop",
|
[GameStatusEnum.Running]: "Stop",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "Resume"
|
[GameStatusEnum.PartiallyInstalled]: "Resume",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||||
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Queued]: QueueListIcon,
|
[GameStatusEnum.Queued]: QueueListIcon,
|
||||||
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
||||||
|
[GameStatusEnum.Validating]: ServerIcon,
|
||||||
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
||||||
[GameStatusEnum.Installed]: PlayIcon,
|
[GameStatusEnum.Installed]: PlayIcon,
|
||||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||||
[GameStatusEnum.Running]: StopIcon,
|
[GameStatusEnum.Running]: StopIcon,
|
||||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
|
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||||
[GameStatusEnum.Remote]: () => emit("install"),
|
[GameStatusEnum.Remote]: () => emit("install"),
|
||||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||||
|
[GameStatusEnum.Validating]: () => emit("queue"),
|
||||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||||
[GameStatusEnum.Uninstalling]: () => {},
|
[GameStatusEnum.Uninstalling]: () => {},
|
||||||
[GameStatusEnum.Running]: () => emit("kill"),
|
[GameStatusEnum.Running]: () => emit("kill"),
|
||||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
|
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
<component class="h-5" :is="item.icon" />
|
<component class="h-5" :is="item.icon" />
|
||||||
</HeaderWidget>
|
</HeaderWidget>
|
||||||
</li>
|
</li>
|
||||||
<OfflineHeaderWidget v-if="state.status === AppStatus.Offline" />
|
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
||||||
<HeaderUserWidget />
|
<HeaderUserWidget />
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Menu v-if="state.user" as="div" class="relative inline-block">
|
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
||||||
<MenuButton>
|
<MenuButton>
|
||||||
<HeaderWidget>
|
<HeaderWidget>
|
||||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||||
>
|
>
|
||||||
<PanelWidget class="flex-col gap-y-2">
|
<div class="flex-col gap-y-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/id/me"
|
to="/id/me"
|
||||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</PanelWidget>
|
</div>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
</transition>
|
</transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
@ -87,7 +87,7 @@ router.afterEach(() => {
|
|||||||
|
|
||||||
const state = useAppState();
|
const state = useAppState();
|
||||||
const profilePictureUrl: string = await useObject(
|
const profilePictureUrl: string = await useObject(
|
||||||
state.value.user?.profilePictureObjectId ?? ""
|
state.value?.user?.profilePictureObjectId ?? ""
|
||||||
);
|
);
|
||||||
const adminUrl: string = await invoke("gen_drop_url", {
|
const adminUrl: string = await invoke("gen_drop_url", {
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
@ -13,11 +13,7 @@
|
|||||||
<div class="max-w-lg">
|
<div class="max-w-lg">
|
||||||
<slot />
|
<slot />
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<button
|
<div>
|
||||||
@click="() => authWrapper_wrapper()"
|
|
||||||
:disabled="loading"
|
|
||||||
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
|
||||||
>
|
|
||||||
<div v-if="loading" role="status">
|
<div v-if="loading" role="status">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -37,10 +33,19 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>
|
<span class="inline-flex gap-x-8 items-center" v-else>
|
||||||
Sign in with your browser <span aria-hidden="true">→</span>
|
<button
|
||||||
</span>
|
@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>
|
</button>
|
||||||
|
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
||||||
|
Use a code →
|
||||||
|
</NuxtLink>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-5" v-if="offerManual">
|
<div class="mt-5" v-if="offerManual">
|
||||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||||
@ -121,6 +126,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
|
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -1,72 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col h-full">
|
||||||
<div class="mb-3 inline-flex gap-x-2">
|
<div class="mb-3 inline-flex gap-x-2">
|
||||||
<div
|
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
|
||||||
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
|
|
||||||
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>
|
</div>
|
||||||
<input
|
<input type="text" v-model="searchQuery"
|
||||||
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"
|
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..."
|
placeholder="Search library..." />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button @click="() => calculateGames(true)"
|
||||||
@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">
|
||||||
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" />
|
<ArrowPathIcon class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||||
<NuxtLink
|
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
|
||||||
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',
|
'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
|
navIndex === currentNavigation
|
||||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||||
: nav.isInstalled.value
|
: nav.isInstalled.value
|
||||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||||
]"
|
]" :href="nav.route">
|
||||||
:href="nav.route"
|
|
||||||
>
|
|
||||||
<div class="flex items-center w-full gap-x-3">
|
<div class="flex items-center w-full gap-x-3">
|
||||||
<div
|
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
|
||||||
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="" />
|
||||||
<img
|
|
||||||
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
|
||||||
:src="icons[nav.id]"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<p
|
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
||||||
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
|
||||||
>
|
|
||||||
{{ nav.label }}
|
{{ nav.label }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
||||||
class="text-xs font-medium"
|
|
||||||
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
|
||||||
>
|
|
||||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</TransitionGroup>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -80,19 +67,21 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
// Style information
|
// Style information
|
||||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Installed]: "text-green-500",
|
[GameStatusEnum.Installed]: "text-green-500",
|
||||||
[GameStatusEnum.Downloading]: "text-blue-500",
|
[GameStatusEnum.Downloading]: "text-zinc-400",
|
||||||
|
[GameStatusEnum.Validating]: "text-blue-300",
|
||||||
[GameStatusEnum.Running]: "text-green-500",
|
[GameStatusEnum.Running]: "text-green-500",
|
||||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||||
[GameStatusEnum.Queued]: "text-blue-500",
|
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||||
[GameStatusEnum.Updating]: "text-blue-500",
|
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-600",
|
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||||
};
|
};
|
||||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Remote]: "Not installed",
|
[GameStatusEnum.Remote]: "Not installed",
|
||||||
[GameStatusEnum.Queued]: "Queued",
|
[GameStatusEnum.Queued]: "Queued",
|
||||||
[GameStatusEnum.Downloading]: "Downloading...",
|
[GameStatusEnum.Downloading]: "Downloading...",
|
||||||
|
[GameStatusEnum.Validating]: "Validating...",
|
||||||
[GameStatusEnum.Installed]: "Installed",
|
[GameStatusEnum.Installed]: "Installed",
|
||||||
[GameStatusEnum.Updating]: "Updating...",
|
[GameStatusEnum.Updating]: "Updating...",
|
||||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||||
@ -105,6 +94,7 @@ const router = useRouter();
|
|||||||
|
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
const games: {
|
const games: {
|
||||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||||
} = {};
|
} = {};
|
||||||
@ -113,7 +103,10 @@ const icons: { [key: string]: string } = {};
|
|||||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||||
|
|
||||||
async function calculateGames(clearAll = false) {
|
async function calculateGames(clearAll = false) {
|
||||||
if (clearAll) rawGames.value = [];
|
if (clearAll) {
|
||||||
|
rawGames.value = [];
|
||||||
|
loading.value = true;
|
||||||
|
}
|
||||||
// If we update immediately, the navigation gets re-rendered before we
|
// If we update immediately, the navigation gets re-rendered before we
|
||||||
// add all the necessary state, and it freaks tf out
|
// add all the necessary state, and it freaks tf out
|
||||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||||
@ -125,10 +118,22 @@ async function calculateGames(clearAll = false) {
|
|||||||
if (icons[game.id]) continue;
|
if (icons[game.id]) continue;
|
||||||
icons[game.id] = await useObject(game.mIconObjectId);
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
rawGames.value = newGames;
|
rawGames.value = newGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
await calculateGames();
|
// 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(() =>
|
const navigation = computed(() =>
|
||||||
rawGames.value.map((game) => {
|
rawGames.value.map((game) => {
|
||||||
@ -136,8 +141,7 @@ const navigation = computed(() =>
|
|||||||
|
|
||||||
const isInstalled = computed(
|
const isInstalled = computed(
|
||||||
() =>
|
() =>
|
||||||
status.value.type == GameStatusEnum.Installed ||
|
status.value.type != GameStatusEnum.Remote
|
||||||
status.value.type == GameStatusEnum.SetupRequired
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
@ -150,9 +154,11 @@ const navigation = computed(() =>
|
|||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
|
|
||||||
navigation.value
|
const route = useRoute();
|
||||||
);
|
const currentNavigation = computed(() => {
|
||||||
|
return navigation.value.findIndex((e) => e.route == route.path)
|
||||||
|
});
|
||||||
|
|
||||||
const filteredNavigation = computed(() => {
|
const filteredNavigation = computed(() => {
|
||||||
if (!searchQuery.value)
|
if (!searchQuery.value)
|
||||||
@ -167,9 +173,7 @@ listen("update_library", async (event) => {
|
|||||||
console.log("Updating library");
|
console.log("Updating library");
|
||||||
let oldNavigation = navigation.value[currentNavigation.value];
|
let oldNavigation = navigation.value[currentNavigation.value];
|
||||||
await calculateGames();
|
await calculateGames();
|
||||||
recalculateNavigation();
|
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
||||||
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
|
||||||
console.log("Triggered");
|
|
||||||
router.push("/library");
|
router.push("/library");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
7
main/components/LoadingIndicator.vue
Normal file
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const loading = useLoadingIndicator();
|
||||||
|
|
||||||
|
watch(loading.isLoading, console.log);
|
||||||
|
</script>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||||
>
|
>
|
||||||
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
||||||
<Wordmark class="mt-1" />
|
<Wordmark class="mt-1" />
|
||||||
3
main/composables/app-state.ts
Normal file
3
main/composables/app-state.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { AppState } from "~/types";
|
||||||
|
|
||||||
|
export const useAppState = () => useState<AppState | undefined>("state");
|
||||||
@ -43,6 +43,7 @@ export const useGame = async (gameId: string) => {
|
|||||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||||
|
|
||||||
listen(`update_game/${gameId}`, (event) => {
|
listen(`update_game/${gameId}`, (event) => {
|
||||||
|
console.log(event);
|
||||||
const payload: {
|
const payload: {
|
||||||
status: SerializedGameStatus;
|
status: SerializedGameStatus;
|
||||||
version?: GameVersion;
|
version?: GameVersion;
|
||||||
@ -65,7 +65,13 @@ export function setupHooks() {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialNavigation(state: Ref<AppState>) {
|
export function initialNavigation(state: ReturnType<typeof useAppState>) {
|
||||||
|
if (!state.value)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: "App state not valid",
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
switch (state.value.status) {
|
switch (state.value.status) {
|
||||||
@ -7,6 +7,7 @@
|
|||||||
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"
|
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" />
|
<Logo class="h-10 w-auto sm:h-12" />
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<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"
|
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"
|
||||||
@ -13,5 +13,9 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
extends: [["./drop-base"]],
|
extends: [["../libs/drop-base"]],
|
||||||
|
|
||||||
|
app: {
|
||||||
|
baseURL: "/main",
|
||||||
|
}
|
||||||
});
|
});
|
||||||
37
main/package.json
Normal file
37
main/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "view",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.3.2",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
37
main/pages/auth/code.vue
Normal file
37
main/pages/auth/code.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<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>
|
||||||
16
main/pages/community/index.vue
Normal file
16
main/pages/community/index.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
||||||
|
<div>
|
||||||
|
<Wordmark />
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
||||||
|
Under construction
|
||||||
|
</h1>
|
||||||
|
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
||||||
|
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
||||||
|
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -76,9 +76,8 @@
|
|||||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||||
<img
|
<img
|
||||||
v-for="(url, index) in mediaUrls"
|
v-for="(url, index) in mediaUrls"
|
||||||
:key="url"
|
:key="index"
|
||||||
:src="url"
|
:src="url"
|
||||||
loading="lazy"
|
|
||||||
class="absolute inset-0 w-full h-full object-cover"
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
v-show="index === currentImageIndex"
|
v-show="index === currentImageIndex"
|
||||||
/>
|
/>
|
||||||
@ -158,8 +157,8 @@
|
|||||||
<template #default>
|
<template #default>
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 class="text-base font-semibold text-zinc-100"
|
<h3 class="text-base font-semibold text-zinc-100">
|
||||||
>Install {{ game.mName }}?
|
Install {{ game.mName }}?
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="text-sm text-zinc-400">
|
<p class="text-sm text-zinc-400">
|
||||||
@ -244,7 +243,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
<div
|
||||||
|
v-else-if="versionOptions === null || versionOptions?.length == 0"
|
||||||
|
class="mt-1 rounded-md bg-red-600/10 p-4"
|
||||||
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||||
@ -257,6 +259,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div v-if="installDirs">
|
||||||
<Listbox as="div" v-model="installDir">
|
<Listbox as="div" v-model="installDir">
|
||||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||||
@ -351,9 +374,7 @@
|
|||||||
<template #buttons>
|
<template #buttons>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@click="() => install()"
|
@click="() => install()"
|
||||||
:disabled="
|
:disabled="!(versionOptions && versionOptions.length > 0)"
|
||||||
!(versionOptions && versionOptions.length > 0)
|
|
||||||
"
|
|
||||||
:loading="installLoading"
|
:loading="installLoading"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="ml-2 w-full sm:w-fit"
|
class="ml-2 w-full sm:w-fit"
|
||||||
@ -371,7 +392,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</ModalTemplate>
|
</ModalTemplate>
|
||||||
|
|
||||||
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
|
<!--
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
enter="transition ease-out duration-300"
|
enter="transition ease-out duration-300"
|
||||||
@ -421,7 +453,7 @@
|
|||||||
<img
|
<img
|
||||||
v-for="(url, index) in mediaUrls"
|
v-for="(url, index) in mediaUrls"
|
||||||
v-show="currentImageIndex === index"
|
v-show="currentImageIndex === index"
|
||||||
:key="url"
|
:key="index"
|
||||||
:src="url"
|
:src="url"
|
||||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||||
@ -442,10 +474,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
TransitionChild,
|
|
||||||
TransitionRoot,
|
|
||||||
Listbox,
|
Listbox,
|
||||||
ListboxButton,
|
ListboxButton,
|
||||||
ListboxLabel,
|
ListboxLabel,
|
||||||
@ -483,7 +511,10 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
|
|||||||
|
|
||||||
// Get all available images
|
// Get all available images
|
||||||
const mediaUrls = await Promise.all(
|
const mediaUrls = await Promise.all(
|
||||||
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||||
|
const src = await useObject(v);
|
||||||
|
return src;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const htmlDescription = micromark(game.value.mDescription);
|
const htmlDescription = micromark(game.value.mDescription);
|
||||||
@ -497,20 +528,19 @@ const currentImageIndex = ref(0);
|
|||||||
|
|
||||||
const configureModalOpen = ref(false);
|
const configureModalOpen = ref(false);
|
||||||
|
|
||||||
|
|
||||||
async function installFlow() {
|
async function installFlow() {
|
||||||
installFlowOpen.value = true;
|
installFlowOpen.value = true;
|
||||||
versionOptions.value = undefined;
|
versionOptions.value = undefined;
|
||||||
installDirs.value = undefined;
|
installDirs.value = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
versionOptions.value = await invoke("fetch_game_version_options", {
|
||||||
gameId: game.value.id,
|
gameId: game.value.id,
|
||||||
});
|
});
|
||||||
console.log(versionOptions.value);
|
|
||||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installError.value = (error as string).toString();
|
installError.value = (error as string).toString();
|
||||||
|
versionOptions.value = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,10 +567,9 @@ async function install() {
|
|||||||
|
|
||||||
async function resumeDownload() {
|
async function resumeDownload() {
|
||||||
try {
|
try {
|
||||||
await invoke("resume_download", { gameId: game.value.id })
|
await invoke("resume_download", { gameId: game.value.id });
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
console.error(e);
|
||||||
console.error(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
16
main/pages/news/index.vue
Normal file
16
main/pages/news/index.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
||||||
|
<div>
|
||||||
|
<Wordmark />
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
||||||
|
Under construction
|
||||||
|
</h1>
|
||||||
|
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
||||||
|
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
||||||
|
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
5
main/tsconfig.json
Normal file
5
main/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"exclude": ["src-tauri/**/*"]
|
||||||
|
}
|
||||||
@ -54,12 +54,13 @@ export enum GameStatusEnum {
|
|||||||
Remote = "Remote",
|
Remote = "Remote",
|
||||||
Queued = "Queued",
|
Queued = "Queued",
|
||||||
Downloading = "Downloading",
|
Downloading = "Downloading",
|
||||||
|
Validating = "Validating",
|
||||||
Installed = "Installed",
|
Installed = "Installed",
|
||||||
Updating = "Updating",
|
Updating = "Updating",
|
||||||
Uninstalling = "Uninstalling",
|
Uninstalling = "Uninstalling",
|
||||||
SetupRequired = "SetupRequired",
|
SetupRequired = "SetupRequired",
|
||||||
Running = "Running",
|
Running = "Running",
|
||||||
PartiallyInstalled = "PartiallyInstalled"
|
PartiallyInstalled = "PartiallyInstalled",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameStatus = {
|
export type GameStatus = {
|
||||||
8091
main/yarn.lock
Normal file
8091
main/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
22
optimize-appimage.sh
Executable file
22
optimize-appimage.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
## 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,46 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0-rc-8",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "node ./build.mjs",
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@tauri-apps/api": "^2.7.0",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
"@tauri-apps/plugin-dialog": "^2.3.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-opener": "^2.4.0",
|
||||||
"@tauri-apps/plugin-os": "~2",
|
"@tauri-apps/plugin-os": "^2.3.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||||
"koa": "^2.16.1",
|
"pino": "^9.7.0",
|
||||||
"markdown-it": "^14.1.0",
|
"pino-pretty": "^13.1.1"
|
||||||
"micromark": "^4.0.1",
|
|
||||||
"nuxt": "^3.16.0",
|
|
||||||
"scss": "^0.2.4",
|
|
||||||
"vue": "latest",
|
|
||||||
"vue-router": "latest",
|
|
||||||
"vuedraggable": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tauri-apps/cli": "^2.7.1"
|
||||||
"@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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
1412
src-tauri/Cargo.lock
generated
1412
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.0-rc-8"
|
version = "0.3.2"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@ -25,6 +25,7 @@ tauri-build = { version = "2.0.0", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri-plugin-shell = "2.2.1"
|
tauri-plugin-shell = "2.2.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
rayon = "1.10.0"
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
@ -67,11 +68,11 @@ known-folders = "1.2.0"
|
|||||||
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-opener = "2.4.0"
|
||||||
bitcode = "0.6.6"
|
bitcode = "0.6.6"
|
||||||
async-trait = "0.1.88"
|
reqwest-websocket = "0.5.0"
|
||||||
futures = "0.3.31"
|
futures-lite = "2.6.0"
|
||||||
tokio-util = { version = "0.7.15", features = ["io"] }
|
page_size = "0.6.0"
|
||||||
async-scoped = { version = "0.9.0", features = ["use-tokio"] }
|
sysinfo = "0.36.1"
|
||||||
async-once-cell = "0.5.4"
|
humansize = "2.1.3"
|
||||||
# tailscale = { path = "./tailscale" }
|
# tailscale = { path = "./tailscale" }
|
||||||
|
|
||||||
[dependencies.dynfmt]
|
[dependencies.dynfmt]
|
||||||
@ -79,7 +80,7 @@ version = "0.1.5"
|
|||||||
features = ["curly"]
|
features = ["curly"]
|
||||||
|
|
||||||
[dependencies.tauri]
|
[dependencies.tauri]
|
||||||
version = "2.1.1"
|
version = "2.7.0"
|
||||||
features = ["protocol-asset", "tray-icon"]
|
features = ["protocol-asset", "tray-icon"]
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
@ -98,14 +99,14 @@ features = ["fs"]
|
|||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||||
|
|
||||||
[dependencies.dropbreak]
|
[dependencies.rustbreak]
|
||||||
git = "https://github.com/Drop-OSS/dropbreak.git"
|
version = "2"
|
||||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12"
|
version = "0.12.22"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "http2", "rustls-tls-webpki-roots", "stream"]
|
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use log::debug;
|
|||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|
||||||
pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
let manager = app.autolaunch();
|
let manager = app.autolaunch();
|
||||||
if enabled {
|
if enabled {
|
||||||
manager.enable().map_err(|e| e.to_string())?;
|
manager.enable().map_err(|e| e.to_string())?;
|
||||||
@ -14,18 +14,16 @@ pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the state in DB
|
// Store the state in DB
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.settings.autostart = enabled;
|
db_handle.settings.autostart = enabled;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_autostart_enabled_logic(
|
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||||
app: AppHandle,
|
|
||||||
) -> Result<bool, tauri_plugin_autostart::Error> {
|
|
||||||
// First check DB state
|
// First check DB state
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let db_state = db_handle.settings.autostart;
|
let db_state = db_handle.settings.autostart;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
@ -46,8 +44,8 @@ pub async fn get_autostart_enabled_logic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New function to sync state on startup
|
// New function to sync state on startup
|
||||||
pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let should_be_enabled = db_handle.settings.autostart;
|
let should_be_enabled = db_handle.settings.autostart;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
@ -67,11 +65,11 @@ pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
toggle_autostart_logic(app, enabled).await
|
toggle_autostart_logic(app, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||||
get_autostart_enabled_logic(app).await
|
get_autostart_enabled_logic(app)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,20 @@
|
|||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::DropFunctionState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn quit<>(app: tauri::AppHandle, state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
cleanup_and_exit(&app, &state).await;
|
cleanup_and_exit(&app, &state);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cleanup_and_exit(
|
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
app: &AppHandle,
|
|
||||||
state: &tauri::State<'_, DropFunctionState<'_>>,
|
|
||||||
) {
|
|
||||||
debug!("cleaning up and exiting application");
|
debug!("cleaning up and exiting application");
|
||||||
let download_manager = state.lock().await.download_manager.clone();
|
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||||
match download_manager.ensure_terminated().await {
|
match download_manager.ensure_terminated() {
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Ok(_) => debug!("download manager terminated correctly"),
|
Ok(()) => debug!("download manager terminated correctly"),
|
||||||
Err(_) => error!("download manager failed to terminate correctly"),
|
Err(()) => error!("download manager failed to terminate correctly"),
|
||||||
},
|
},
|
||||||
Err(e) => panic!("{e:?}"),
|
Err(e) => panic!("{e:?}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use crate::DropFunctionState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_state(
|
pub fn fetch_state(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let guard = state.lock().await;
|
let guard = state.lock().unwrap();
|
||||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
|
|||||||
@ -7,11 +7,11 @@ use std::{
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
|
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
db::{DATA_ROOT_DIR, borrow_db_checked},
|
db::{borrow_db_checked, DATA_ROOT_DIR},
|
||||||
debug::SystemData,
|
debug::SystemData,
|
||||||
models::data::Settings,
|
models::data::Settings,
|
||||||
};
|
};
|
||||||
@ -19,19 +19,19 @@ use super::{
|
|||||||
// Will, in future, return disk/remaining size
|
// Will, in future, return disk/remaining size
|
||||||
// Just returns the directories that have been set up
|
// Just returns the directories that have been set up
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||||
let lock = borrow_db_checked().await;
|
let lock = borrow_db_checked();
|
||||||
lock.applications.install_dirs.clone()
|
lock.applications.install_dirs.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_download_dir(index: usize) {
|
pub fn delete_download_dir(index: usize) {
|
||||||
let mut lock = borrow_db_mut_checked().await;
|
let mut lock = borrow_db_mut_checked();
|
||||||
lock.applications.install_dirs.remove(index);
|
lock.applications.install_dirs.remove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
||||||
// Check the new directory is all good
|
// Check the new directory is all good
|
||||||
let new_dir_path = Path::new(&new_dir);
|
let new_dir_path = Path::new(&new_dir);
|
||||||
if new_dir_path.exists() {
|
if new_dir_path.exists() {
|
||||||
@ -48,7 +48,7 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add it to the dictionary
|
// Add it to the dictionary
|
||||||
let mut lock = borrow_db_mut_checked().await;
|
let mut lock = borrow_db_mut_checked();
|
||||||
if lock.applications.install_dirs.contains(&new_dir) {
|
if lock.applications.install_dirs.contains(&new_dir) {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::AlreadyExists,
|
ErrorKind::AlreadyExists,
|
||||||
@ -59,12 +59,14 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
|||||||
lock.applications.install_dirs.push(new_dir);
|
lock.applications.install_dirs.push(new_dir);
|
||||||
drop(lock);
|
drop(lock);
|
||||||
|
|
||||||
|
scan_install_dirs();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_settings(new_settings: Value) {
|
pub fn update_settings(new_settings: Value) {
|
||||||
let mut db_lock = borrow_db_mut_checked().await;
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
||||||
for (key, value) in new_settings.as_object().unwrap() {
|
for (key, value) in new_settings.as_object().unwrap() {
|
||||||
current_settings[key] = value.clone();
|
current_settings[key] = value.clone();
|
||||||
@ -73,12 +75,12 @@ pub async fn update_settings(new_settings: Value) {
|
|||||||
db_lock.settings = new_settings;
|
db_lock.settings = new_settings;
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_settings() -> Settings {
|
pub fn fetch_settings() -> Settings {
|
||||||
borrow_db_checked().await.settings.clone()
|
borrow_db_checked().settings.clone()
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_system_data() -> SystemData {
|
pub fn fetch_system_data() -> SystemData {
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
SystemData::new(
|
SystemData::new(
|
||||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||||
db_handle.base_url.clone(),
|
db_handle.base_url.clone(),
|
||||||
|
|||||||
@ -3,19 +3,14 @@ use std::{
|
|||||||
mem::ManuallyDrop,
|
mem::ManuallyDrop,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_once_cell::OnceCell;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use dropbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
use log::{debug, error, info, warn};
|
||||||
use log::{debug, info, warn};
|
|
||||||
use native_model::{Decode, Encode};
|
use native_model::{Decode, Encode};
|
||||||
|
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use tokio::{
|
|
||||||
spawn,
|
|
||||||
sync::{RwLockReadGuard, RwLockWriteGuard},
|
|
||||||
};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
@ -32,15 +27,15 @@ pub struct DropDatabaseSerializer;
|
|||||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||||
for DropDatabaseSerializer
|
for DropDatabaseSerializer
|
||||||
{
|
{
|
||||||
fn serialize(&self, val: &T) -> dropbreak::error::DeSerResult<Vec<u8>> {
|
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||||
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
.map_err(|e| DeSerError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> dropbreak::error::DeSerResult<T> {
|
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
s.read_to_end(&mut buf)
|
s.read_to_end(&mut buf)
|
||||||
.map_err(|e| dropbreak::error::DeSerError::Other(e.into()))?;
|
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||||
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
Ok(val)
|
Ok(val)
|
||||||
@ -48,33 +43,15 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub type DatabaseInterface =
|
pub type DatabaseInterface =
|
||||||
dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer>;
|
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||||
|
|
||||||
pub struct OnceCellDatabase(OnceCell<DatabaseInterface>);
|
|
||||||
impl OnceCellDatabase {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self(OnceCell::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(&self, init: impl Future<Output = DatabaseInterface>) {
|
|
||||||
self.0.get_or_init(init).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Deref for OnceCellDatabase {
|
|
||||||
type Target = DatabaseInterface;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0.get().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DatabaseImpls {
|
pub trait DatabaseImpls {
|
||||||
async fn set_up_database() -> DatabaseInterface;
|
fn set_up_database() -> DatabaseInterface;
|
||||||
async fn database_is_set_up(&self) -> bool;
|
fn database_is_set_up(&self) -> bool;
|
||||||
async fn fetch_base_url(&self) -> Url;
|
fn fetch_base_url(&self) -> Url;
|
||||||
}
|
}
|
||||||
impl DatabaseImpls for DatabaseInterface {
|
impl DatabaseImpls for DatabaseInterface {
|
||||||
async fn set_up_database() -> DatabaseInterface {
|
fn set_up_database() -> DatabaseInterface {
|
||||||
let db_path = DATA_ROOT_DIR.join("drop.db");
|
let db_path = DATA_ROOT_DIR.join("drop.db");
|
||||||
let games_base_dir = DATA_ROOT_DIR.join("games");
|
let games_base_dir = DATA_ROOT_DIR.join("games");
|
||||||
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
||||||
@ -90,40 +67,38 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
|
|
||||||
let exists = fs::exists(db_path.clone()).unwrap();
|
let exists = fs::exists(db_path.clone()).unwrap();
|
||||||
|
|
||||||
match exists {
|
if exists {
|
||||||
true => match PathDatabase::load_from_path(db_path.clone()).await {
|
match PathDatabase::load_from_path(db_path.clone()) {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir).await,
|
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||||
},
|
}
|
||||||
false => {
|
} else {
|
||||||
let default = Database::new(games_base_dir, None, cache_dir);
|
let default = Database::new(games_base_dir, None, cache_dir);
|
||||||
debug!(
|
debug!(
|
||||||
"Creating database at path {}",
|
"Creating database at path {}",
|
||||||
db_path.as_os_str().to_str().unwrap()
|
db_path.as_os_str().to_str().unwrap()
|
||||||
);
|
);
|
||||||
PathDatabase::create_at_path(db_path, default)
|
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
||||||
.await
|
|
||||||
.expect("Database could not be created")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn database_is_set_up(&self) -> bool {
|
fn database_is_set_up(&self) -> bool {
|
||||||
!self.borrow_data().await.base_url.is_empty()
|
!self.borrow_data().unwrap().base_url.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_base_url(&self) -> Url {
|
fn fetch_base_url(&self) -> Url {
|
||||||
let handle = self.borrow_data().await;
|
let handle = self.borrow_data().unwrap();
|
||||||
Url::parse(&handle.base_url).unwrap()
|
Url::parse(&handle.base_url).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_invalid_database(
|
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||||
|
fn handle_invalid_database(
|
||||||
_e: RustbreakError,
|
_e: RustbreakError,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
games_base_dir: PathBuf,
|
games_base_dir: PathBuf,
|
||||||
cache_dir: PathBuf,
|
cache_dir: PathBuf,
|
||||||
) -> dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer> {
|
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||||
warn!("{_e}");
|
warn!("{_e}");
|
||||||
let new_path = {
|
let new_path = {
|
||||||
let time = Utc::now().timestamp();
|
let time = Utc::now().timestamp();
|
||||||
@ -140,23 +115,14 @@ async fn handle_invalid_database(
|
|||||||
cache_dir,
|
cache_dir,
|
||||||
);
|
);
|
||||||
|
|
||||||
PathDatabase::create_at_path(db_path, db)
|
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||||
.await
|
|
||||||
.expect("Database could not be created")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// To automatically save the database upon drop
|
// To automatically save the database upon drop
|
||||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||||
impl<'a> Deref for DBWrite<'a> {
|
impl<'a> Deref for DBWrite<'a> {
|
||||||
type Target = RwLockWriteGuard<'a, Database>;
|
type Target = Database;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Deref for DBRead<'a> {
|
|
||||||
type Target = RwLockReadGuard<'a, Database>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
@ -167,26 +133,45 @@ impl<'a> DerefMut for DBWrite<'a> {
|
|||||||
&mut self.0
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> Drop for DBWrite<'a> {
|
impl<'a> Deref for DBRead<'a> {
|
||||||
|
type Target = Database;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for DBWrite<'_> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
ManuallyDrop::drop(&mut self.0);
|
ManuallyDrop::drop(&mut self.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn(async {
|
match DB.save() {
|
||||||
match DB.save().await {
|
Ok(()) => {}
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
error!("database failed to save with error {e}");
|
||||||
panic!("database failed to save with error {e}")
|
panic!("database failed to save with error {e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn borrow_db_checked<'a>() -> DBRead<'a> {
|
|
||||||
DBRead(DB.borrow_data().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||||
DBWrite(ManuallyDrop::new(DB.borrow_data_mut().await))
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,3 +2,4 @@ pub mod commands;
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
pub mod scan;
|
||||||
@ -1,8 +1,14 @@
|
|||||||
use crate::database::models::data::Database;
|
/**
|
||||||
|
* NEXT BREAKING CHANGE
|
||||||
|
*
|
||||||
|
* UPDATE DATABASE TO USE RPMSERDENAMED
|
||||||
|
*
|
||||||
|
* WE CAN'T DELETE ANY FIELDS
|
||||||
|
*/
|
||||||
pub mod data {
|
pub mod data {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
||||||
use native_model::native_model;
|
use native_model::native_model;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -18,16 +24,14 @@ pub mod data {
|
|||||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||||
|
|
||||||
use std::{collections::HashMap, process::Command};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
|
||||||
|
|
||||||
pub mod v1 {
|
pub mod v1 {
|
||||||
use crate::process::process_manager::Platform;
|
use crate::process::process_manager::Platform;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use super::*;
|
use super::{Deserialize, Serialize, native_model};
|
||||||
|
|
||||||
fn default_template() -> String {
|
fn default_template() -> String {
|
||||||
"{}".to_owned()
|
"{}".to_owned()
|
||||||
@ -115,6 +119,7 @@ pub mod data {
|
|||||||
Downloading { version_name: String },
|
Downloading { version_name: String },
|
||||||
Uninstalling {},
|
Uninstalling {},
|
||||||
Updating { version_name: String },
|
Updating { version_name: String },
|
||||||
|
Validating { version_name: String },
|
||||||
Running {},
|
Running {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +179,10 @@ pub mod data {
|
|||||||
|
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use super::*;
|
use super::{
|
||||||
|
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
|
||||||
|
GameVersion, Serialize, Settings, native_model, v1,
|
||||||
|
};
|
||||||
|
|
||||||
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
@ -206,7 +214,7 @@ pub mod data {
|
|||||||
applications: value.applications,
|
applications: value.applications,
|
||||||
prev_database: value.prev_database,
|
prev_database: value.prev_database,
|
||||||
cache_dir: value.cache_dir,
|
cache_dir: value.cache_dir,
|
||||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
compat_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,6 +266,7 @@ pub mod data {
|
|||||||
pub install_dirs: Vec<PathBuf>,
|
pub install_dirs: Vec<PathBuf>,
|
||||||
// Guaranteed to exist if the game also exists in the app state map
|
// Guaranteed to exist if the game also exists in the app state map
|
||||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||||
|
|
||||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||||
|
|
||||||
@ -283,7 +292,10 @@ pub mod data {
|
|||||||
mod v3 {
|
mod v3 {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::*;
|
use super::{
|
||||||
|
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
||||||
|
Settings, native_model, v2,
|
||||||
|
};
|
||||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
@ -297,6 +309,7 @@ pub mod data {
|
|||||||
pub cache_dir: PathBuf,
|
pub cache_dir: PathBuf,
|
||||||
pub compat_info: Option<DatabaseCompatInfo>,
|
pub compat_info: Option<DatabaseCompatInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<v2::Database> for Database {
|
impl From<v2::Database> for Database {
|
||||||
fn from(value: v2::Database) -> Self {
|
fn from(value: v2::Database) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -306,22 +319,13 @@ pub mod data {
|
|||||||
applications: value.applications.into(),
|
applications: value.applications.into(),
|
||||||
prev_database: value.prev_database,
|
prev_database: value.prev_database,
|
||||||
cache_dir: value.cache_dir,
|
cache_dir: value.cache_dir,
|
||||||
compat_info: Database::create_new_compat_info(),
|
compat_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
return None;
|
|
||||||
|
|
||||||
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
|
|
||||||
Some(DatabaseCompatInfo {
|
|
||||||
umu_installed: has_umu_installed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<T: Into<PathBuf>>(
|
pub fn new<T: Into<PathBuf>>(
|
||||||
games_base_dir: T,
|
games_base_dir: T,
|
||||||
prev_database: Option<PathBuf>,
|
prev_database: Option<PathBuf>,
|
||||||
@ -336,11 +340,11 @@ pub mod data {
|
|||||||
transient_statuses: HashMap::new(),
|
transient_statuses: HashMap::new(),
|
||||||
},
|
},
|
||||||
prev_database,
|
prev_database,
|
||||||
base_url: "".to_owned(),
|
base_url: String::new(),
|
||||||
auth: None,
|
auth: None,
|
||||||
settings: Settings::default(),
|
settings: Settings::default(),
|
||||||
cache_dir,
|
cache_dir,
|
||||||
compat_info: Database::create_new_compat_info(),
|
compat_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src-tauri/src/database/scan.rs
Normal file
52
src-tauri/src/database/scan.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{
|
||||||
|
db::borrow_db_mut_checked,
|
||||||
|
models::data::v1::{DownloadType, DownloadableMetadata},
|
||||||
|
},
|
||||||
|
games::{
|
||||||
|
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
|
||||||
|
library::set_partially_installed_db,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn scan_install_dirs() {
|
||||||
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
|
for install_dir in db_lock.applications.install_dirs.clone() {
|
||||||
|
let Ok(files) = fs::read_dir(install_dir) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for game in files.into_iter().flatten() {
|
||||||
|
let drop_data_file = game.path().join(DROP_DATA_PATH);
|
||||||
|
if !drop_data_file.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let game_id = game.file_name().into_string().unwrap();
|
||||||
|
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||||
|
warn!(
|
||||||
|
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||||
|
game.file_name().into_string().unwrap()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if db_lock.applications.game_statuses.contains_key(&game_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = DownloadableMetadata::new(
|
||||||
|
drop_data.game_id,
|
||||||
|
Some(drop_data.game_version),
|
||||||
|
DownloadType::Game,
|
||||||
|
);
|
||||||
|
set_partially_installed_db(
|
||||||
|
&mut db_lock,
|
||||||
|
&metadata,
|
||||||
|
drop_data.base_path.to_str().unwrap().to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,33 +1,31 @@
|
|||||||
use crate::{database::models::data::DownloadableMetadata, DropFunctionState};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn pause_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().await.download_manager.pause_downloads();
|
state.lock().unwrap().download_manager.pause_downloads();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn resume_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().await.download_manager.resume_downloads();
|
state.lock().unwrap().download_manager.resume_downloads();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn move_download_in_queue(
|
pub fn move_download_in_queue(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
old_index: usize,
|
old_index: usize,
|
||||||
new_index: usize,
|
new_index: usize,
|
||||||
) -> Result<(), ()> {
|
) {
|
||||||
state
|
state
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.unwrap()
|
||||||
.download_manager
|
.download_manager
|
||||||
.rearrange(old_index, new_index);
|
.rearrange(old_index, new_index);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cancel_game(state: tauri::State<'_, DropFunctionState<'_>>, meta: DownloadableMetadata) -> Result<(), ()> {
|
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||||
state.lock().await.download_manager.cancel(meta);
|
state.lock().unwrap().download_manager.cancel(meta);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc, Mutex,
|
||||||
mpsc::{Receiver, Sender, channel},
|
mpsc::{Receiver, Sender, channel},
|
||||||
},
|
},
|
||||||
|
thread::{JoinHandle, spawn},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::futures::future::join_all;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::{runtime::Runtime, sync::Mutex};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
database::models::data::DownloadableMetadata,
|
||||||
@ -70,15 +69,14 @@ Behold, my madness - quexeky
|
|||||||
pub struct DownloadManagerBuilder {
|
pub struct DownloadManagerBuilder {
|
||||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||||
download_queue: Queue,
|
download_queue: Queue,
|
||||||
command_receiver: Mutex<Receiver<DownloadManagerSignal>>,
|
command_receiver: Receiver<DownloadManagerSignal>,
|
||||||
sender: Sender<DownloadManagerSignal>,
|
sender: Sender<DownloadManagerSignal>,
|
||||||
progress: CurrentProgressObject,
|
progress: CurrentProgressObject,
|
||||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
runtime: Runtime,
|
|
||||||
|
|
||||||
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
||||||
current_download_thread: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||||
active_control_flag: Option<DownloadThreadControl>,
|
active_control_flag: Option<DownloadThreadControl>,
|
||||||
}
|
}
|
||||||
impl DownloadManagerBuilder {
|
impl DownloadManagerBuilder {
|
||||||
@ -91,110 +89,104 @@ impl DownloadManagerBuilder {
|
|||||||
let manager = Self {
|
let manager = Self {
|
||||||
download_agent_registry: HashMap::new(),
|
download_agent_registry: HashMap::new(),
|
||||||
download_queue: queue.clone(),
|
download_queue: queue.clone(),
|
||||||
command_receiver: Mutex::new(command_receiver),
|
command_receiver,
|
||||||
status: status.clone(),
|
status: status.clone(),
|
||||||
sender: command_sender.clone(),
|
sender: command_sender.clone(),
|
||||||
progress: active_progress.clone(),
|
progress: active_progress.clone(),
|
||||||
app_handle,
|
app_handle,
|
||||||
runtime: tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.worker_threads(1)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
|
|
||||||
current_download_agent: None,
|
current_download_agent: None,
|
||||||
current_download_thread: Mutex::new(None),
|
current_download_thread: Mutex::new(None),
|
||||||
active_control_flag: None,
|
active_control_flag: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let terminator = tauri::async_runtime::spawn(async {
|
let terminator = spawn(|| manager.manage_queue());
|
||||||
if let Err(_err) = manager.manage_queue().await {
|
|
||||||
panic!("download manager exited with error");
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_status(&self, status: DownloadManagerStatus) {
|
fn set_status(&self, status: DownloadManagerStatus) {
|
||||||
*self.status.lock().await = status;
|
*self.status.lock().unwrap() = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_and_cleanup_front_download(
|
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||||
&mut self,
|
|
||||||
meta: &DownloadableMetadata,
|
|
||||||
) -> DownloadAgent {
|
|
||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
||||||
self.cleanup_current_download().await;
|
self.cleanup_current_download();
|
||||||
download_agent
|
download_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAREFUL WITH THIS FUNCTION
|
// CAREFUL WITH THIS FUNCTION
|
||||||
// Make sure the download thread is terminated
|
// Make sure the download thread is terminated
|
||||||
async fn cleanup_current_download(&mut self) {
|
fn cleanup_current_download(&mut self) {
|
||||||
self.active_control_flag = None;
|
self.active_control_flag = None;
|
||||||
*self.progress.lock().await = None;
|
*self.progress.lock().unwrap() = None;
|
||||||
self.current_download_agent = None;
|
self.current_download_agent = None;
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
*download_thread_lock = None;
|
|
||||||
|
if let Some(unfinished_thread) = download_thread_lock.take()
|
||||||
|
&& !unfinished_thread.is_finished()
|
||||||
|
{
|
||||||
|
unfinished_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
drop(download_thread_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_and_wait_current_download(&self) {
|
fn stop_and_wait_current_download(&self) -> bool {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
if let Some(current_flag) = &self.active_control_flag {
|
if let Some(current_flag) = &self.active_control_flag {
|
||||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||||
current_download_thread.await.unwrap();
|
return current_download_thread.join().is_ok();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn manage_queue(mut self) -> Result<(), ()> {
|
fn manage_queue(mut self) -> Result<(), ()> {
|
||||||
loop {
|
loop {
|
||||||
let signal = match self.command_receiver.lock().await.recv() {
|
let signal = match self.command_receiver.recv() {
|
||||||
Ok(signal) => signal,
|
Ok(signal) => signal,
|
||||||
Err(_) => return Err(()),
|
Err(_) => return Err(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match signal {
|
match signal {
|
||||||
DownloadManagerSignal::Go => {
|
DownloadManagerSignal::Go => {
|
||||||
self.manage_go_signal().await;
|
self.manage_go_signal();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Stop => {
|
DownloadManagerSignal::Stop => {
|
||||||
self.manage_stop_signal().await;
|
self.manage_stop_signal();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Completed(meta) => {
|
DownloadManagerSignal::Completed(meta) => {
|
||||||
self.manage_completed_signal(meta).await;
|
self.manage_completed_signal(meta);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Queue(download_agent) => {
|
DownloadManagerSignal::Queue(download_agent) => {
|
||||||
self.manage_queue_signal(download_agent).await;
|
self.manage_queue_signal(download_agent);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Error(e) => {
|
DownloadManagerSignal::Error(e) => {
|
||||||
self.manage_error_signal(e).await;
|
self.manage_error_signal(e);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::UpdateUIQueue => {
|
DownloadManagerSignal::UpdateUIQueue => {
|
||||||
self.push_ui_queue_update().await;
|
self.push_ui_queue_update();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||||
self.push_ui_stats_update(kbs, time);
|
self.push_ui_stats_update(kbs, time);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Finish => {
|
DownloadManagerSignal::Finish => {
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Cancel(meta) => {
|
DownloadManagerSignal::Cancel(meta) => {
|
||||||
self.manage_cancel_signal(&meta).await;
|
self.manage_cancel_signal(&meta);
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
}
|
||||||
|
}
|
||||||
|
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||||
debug!("got signal Queue");
|
debug!("got signal Queue");
|
||||||
let meta = download_agent.metadata();
|
let meta = download_agent.metadata();
|
||||||
|
|
||||||
@ -205,7 +197,7 @@ impl DownloadManagerBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
download_agent.on_initialised(&self.app_handle).await;
|
download_agent.on_initialised(&self.app_handle);
|
||||||
self.download_queue.append(meta.clone());
|
self.download_queue.append(meta.clone());
|
||||||
self.download_agent_registry.insert(meta, download_agent);
|
self.download_agent_registry.insert(meta, download_agent);
|
||||||
|
|
||||||
@ -214,7 +206,7 @@ impl DownloadManagerBuilder {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn manage_go_signal(&mut self) {
|
fn manage_go_signal(&mut self) {
|
||||||
debug!("got signal Go");
|
debug!("got signal Go");
|
||||||
if self.download_agent_registry.is_empty() {
|
if self.download_agent_registry.is_empty() {
|
||||||
debug!(
|
debug!(
|
||||||
@ -228,10 +220,6 @@ impl DownloadManagerBuilder {
|
|||||||
&& self.download_queue.read().front().unwrap()
|
&& self.download_queue.read().front().unwrap()
|
||||||
== &self.current_download_agent.as_ref().unwrap().metadata()
|
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
{
|
{
|
||||||
debug!(
|
|
||||||
"Current download agent: {:?}",
|
|
||||||
self.current_download_agent.as_ref().unwrap().metadata()
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,117 +236,119 @@ impl DownloadManagerBuilder {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
self.active_control_flag = Some(download_agent.control_flag().await);
|
self.active_control_flag = Some(download_agent.control_flag());
|
||||||
self.current_download_agent = Some(download_agent.clone());
|
self.current_download_agent = Some(download_agent.clone());
|
||||||
|
|
||||||
info!("fetched control flag");
|
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
info!("cloned sender");
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
|
||||||
info!("acquired download thread lock");
|
|
||||||
let app_handle = self.app_handle.clone();
|
let app_handle = self.app_handle.clone();
|
||||||
|
|
||||||
info!("starting download agent thread");
|
*download_thread_lock = Some(spawn(move || {
|
||||||
|
loop {
|
||||||
*download_thread_lock = Some(self.runtime.spawn(async move {
|
let download_result = match download_agent.download(&app_handle) {
|
||||||
info!("started download agent thread");
|
|
||||||
match download_agent.download(&app_handle).await {
|
|
||||||
// Ok(true) is for completed and exited properly
|
// Ok(true) is for completed and exited properly
|
||||||
Ok(true) => {
|
Ok(v) => v,
|
||||||
debug!("download {:?} has completed", download_agent.metadata());
|
Err(e) => {
|
||||||
match download_agent.validate().await {
|
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||||
Ok(true) => {
|
download_agent.on_error(&app_handle, &e);
|
||||||
download_agent.on_complete(&app_handle).await;
|
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||||
sender
|
return;
|
||||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
};
|
||||||
download_agent.on_incomplete(&app_handle).await;
|
|
||||||
|
// 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) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"download {:?} has validation error {}",
|
"download {:?} has validation error {}",
|
||||||
download_agent.metadata(),
|
download_agent.metadata(),
|
||||||
&e
|
&e
|
||||||
);
|
);
|
||||||
download_agent.on_error(&app_handle, &e).await;
|
download_agent.on_error(&app_handle, &e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ok(false) is for incomplete but exited properly
|
|
||||||
Ok(false) => {
|
|
||||||
debug!("Donwload agent finished incomplete");
|
|
||||||
download_agent.on_incomplete(&app_handle).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
|
||||||
download_agent.on_error(&app_handle, &e).await;
|
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
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();
|
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.set_status(DownloadManagerStatus::Downloading).await;
|
self.set_status(DownloadManagerStatus::Downloading);
|
||||||
let active_control_flag = self.active_control_flag.clone().unwrap();
|
let active_control_flag = self.active_control_flag.clone().unwrap();
|
||||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||||
|
|
||||||
// download_thread_lock.take().unwrap().await;
|
|
||||||
}
|
}
|
||||||
async fn manage_stop_signal(&mut self) {
|
fn manage_stop_signal(&mut self) {
|
||||||
debug!("got signal Stop");
|
debug!("got signal Stop");
|
||||||
|
|
||||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||||
debug!("got signal Completed");
|
debug!("got signal Completed");
|
||||||
if let Some(interface) = &self.current_download_agent
|
if let Some(interface) = &self.current_download_agent
|
||||||
&& interface.metadata() == meta
|
&& interface.metadata() == meta
|
||||||
{
|
{
|
||||||
self.remove_and_cleanup_front_download(&meta).await;
|
self.remove_and_cleanup_front_download(&meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_ui_queue_update().await;
|
self.push_ui_queue_update();
|
||||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||||
}
|
}
|
||||||
async fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||||
debug!("got signal Error");
|
debug!("got signal Error");
|
||||||
if let Some(current_agent) = self.current_download_agent.clone() {
|
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||||
current_agent.on_error(&self.app_handle, &error).await;
|
current_agent.on_error(&self.app_handle, &error);
|
||||||
|
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
self.remove_and_cleanup_front_download(¤t_agent.metadata())
|
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
self.set_status(DownloadManagerStatus::Error).await;
|
self.push_ui_queue_update();
|
||||||
|
self.set_status(DownloadManagerStatus::Error);
|
||||||
}
|
}
|
||||||
async fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||||
debug!("got signal Cancel");
|
debug!("got signal Cancel");
|
||||||
|
|
||||||
if let Some(current_download) = &self.current_download_agent {
|
if let Some(current_download) = &self.current_download_agent {
|
||||||
if ¤t_download.metadata() == meta {
|
if ¤t_download.metadata() == meta {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
current_download.on_cancelled(&self.app_handle).await;
|
current_download.on_cancelled(&self.app_handle);
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
|
|
||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
|
|
||||||
self.cleanup_current_download().await;
|
self.cleanup_current_download();
|
||||||
debug!("current download queue: {:?}", self.download_queue.read());
|
debug!("current download queue: {:?}", self.download_queue.read());
|
||||||
}
|
}
|
||||||
// TODO: Collapse these two into a single if statement somehow
|
// TODO: Collapse these two into a single if statement somehow
|
||||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
download_agent.on_cancelled(&self.app_handle).await;
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||||
let removed = self.download_agent_registry.remove(meta);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
@ -371,7 +361,7 @@ impl DownloadManagerBuilder {
|
|||||||
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
download_agent.on_cancelled(&self.app_handle).await;
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||||
let removed = self.download_agent_registry.remove(meta);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
@ -381,26 +371,28 @@ impl DownloadManagerBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_ui_queue_update().await;
|
self.push_ui_queue_update();
|
||||||
}
|
}
|
||||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||||
|
|
||||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||||
}
|
}
|
||||||
async fn push_ui_queue_update(&self) {
|
fn push_ui_queue_update(&self) {
|
||||||
let queue = &self.download_queue.read();
|
let queue = &self.download_queue.read();
|
||||||
let queue_objs = join_all(queue.iter().map(async |key| {
|
let queue_objs = queue
|
||||||
|
.iter()
|
||||||
|
.map(|key| {
|
||||||
let val = self.download_agent_registry.get(key).unwrap();
|
let val = self.download_agent_registry.get(key).unwrap();
|
||||||
QueueUpdateEventQueueData {
|
QueueUpdateEventQueueData {
|
||||||
meta: DownloadableMetadata::clone(key),
|
meta: DownloadableMetadata::clone(key),
|
||||||
status: val.status().await,
|
status: val.status(),
|
||||||
progress: val.progress().await.get_progress(),
|
progress: val.progress().get_progress(),
|
||||||
current: val.progress().await.sum(),
|
current: val.progress().sum(),
|
||||||
max: val.progress().await.get_max(),
|
max: val.progress().get_max(),
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
.await;
|
.collect();
|
||||||
|
|
||||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user