Compare commits

..

19 Commits

Author SHA1 Message Date
96df57ac54 chore: Add "Under Construction" page for community and news
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-19 12:17:41 +10:00
8069616f2b feat: add umu executable finding (#122)
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-19 10:52:24 +10:00
5eef2bf60f Fix Tauri builds (#119)
* fix: attempt to use local user keychain

* chore: tmp disable non-macos builds

* fix: windows process fix patch

* fix: re-enable windows

* fix: remove sudo

* fix: tmp disable windows again

* fix: windows build again

* chore: re-disable windows

* fix: pin to macos 14

* fix: re-enable other builds
2025-08-15 23:55:34 +10:00
ec6294b8e7 Fix #117 (#118)
* fix: version data not being attached to process manager push

* fix: clippy
2025-08-15 23:02:01 +10:00
17c375bcab UI & error fixes & QoL (#116)
* fix: use Arc<Error> instead of just ErrorKind

* fix: game status updates for UI

* fix: missing game version on push_game_update calls

* feat: wait if library load takes <300ms

* fix: clippy
2025-08-15 22:56:49 +10:00
cb55ac2bf5 Fix platform builds 2025-08-12 15:08:50 +10:00
e11db851a5 fix: #92 (#115) 2025-08-11 14:37:46 +10:00
16365713cf v2 download API and fixes (#112)
* fix: potential download fixes

* fix: show installed games not on remote

* fix: more download_logic error handling

* partial: move to async

* feat: interactivity improvements

* feat: v2 download API

* fix: download seek offsets

* fix: clippy

* fix: apply clippy suggestion

* fix: performance improvements starting up download

* fix: finished bucket file

* fix: ui tweaks and fixes

* fix: revert version to 0.3.2

* fix: clippy
2025-08-09 15:50:21 +10:00
3b830e2a44 Move frontend to main folder (#109)
* feat: small refactor

* fix: appimage build script

* fix: add NO_STRIP to AppImage build

* fix: build and dev mode from refactor

* fix: submodule step 1

* fix: submodules step 2
2025-08-05 16:09:47 +10:00
75a4b73ee1 QoL Download Manager (#108)
* feat: retry specific download errors

* fix: potential fix for cmd window on Windows

* feat: add disk space check for download

* fix: update game fix formatting

* fix: clippy
2025-08-04 16:30:45 +10:00
339d707092 Fix errors with caching when cache is deleted (#101) 2025-08-04 15:02:32 +10:00
776dc8fe7a Fixes reqwest client setup, #87 (#107) 2025-08-04 15:01:44 +10:00
dbe8c8df4d Process manager templating & game importing (#96)
* feat: add new template options, asahi support, and refactoring

* feat: install dir scanning, validation fixes, progress fixes, download manager refactor

This kind of ballooned out of scope, but I implemented some much
needed fixes for the download manager.

First off, I cleanup the Downloadable trait, there was some
duplication of function.

Second, I refactored the "validate" into the GameDownloadAgent,
which calls a 'validate_chunk_logic' yada, same structure as
downloading.

Third, I fixed the progress and validation issues.

Fourth, I added game scanning

* feat: out of box support for Asahi Linux

* fix: clippy

* fix: don't break database
2025-08-02 20:17:27 +10:00
35f49b8811 macOS app signing (#95)
* feat: add macos signing args

* fix: update all versions to -mac specific

* fix: fetch signing identity

* feat: add signing pre-steps like the docs say

* fix: remove apple requirement from signing

* fix: add drop cert to keychain when signing

* fix: add drop.pem to add-trusted-cert

* fix: re-order and specify import operation

* fix: let's try the user store

* fix: password required to update trust

* fix: try another non-interactive fix

* fix: try sudo

* fix: revert attempt fix

* fix: add cert id debug

* fix: attempt to use id rather than name

* fix: revert code id to name
2025-08-02 15:01:53 +10:00
cc5339a389 Reqwest optionally load certificates from disk (#94)
* feat: Add get_client function

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Converted all instances of reqwest::blocking::Client::new() and reqwest::Client::new() to DROP_CLIENT_SYNC and DROP_CLIENT_ASYNC respectively

Signed-off-by: quexeky <git@quexeky.dev>

* fix: use_remote_logic not using certificates

Signed-off-by: quexeky <git@quexeky.dev>

* fix: add log statement to certificates

* chore: add more logging

* fix: clippy

* refactor: into single fetch_certificates func

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-02 11:59:50 +10:00
6104bfda72 Bump version to v0.3.1 (#85) 2025-08-01 14:13:13 +10:00
be688cb18f Version bump: v0.3.0 2025-08-01 14:09:16 +10:00
13cc69f10e Device code authorization (#83)
* feat: device code authorization

* Fix for setup executable unable to be launched (#81)

* Fix for redownload invalid chunks (#84)

* feat: Redownloading invalid chunks

Signed-off-by: quexeky <git@quexeky.dev>

* fix: clippy

* fix: clippy x2

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>

* chore: Run clippy fix pedantic

Signed-off-by: quexeky <git@quexeky.dev>

* feat: add better error handling

* fix: clippy

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-01 13:12:05 +10:00
574782f445 chore: Run clippy fix pedantic
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-01 08:42:45 +10:00
135 changed files with 10891 additions and 9482 deletions

View File

@ -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
View File

@ -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
View File

@ -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
View 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,
});
}

View File

@ -1,3 +0,0 @@
import type { AppState } from "~/types";
export const useAppState = () => useState<AppState>("state");

View File

@ -10,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,
@ -21,18 +19,26 @@ import {
const router = useRouter(); const router = useRouter();
const state = useAppState(); const state = useAppState();
try {
state.value = JSON.parse(await invoke("fetch_state"));
} 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();

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -1,52 +1,78 @@
<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
styles[props.status.type], type="button"
showDropdown ? 'rounded-l-md' : 'rounded-md', @click="() => buttonActions[props.status.type]()"
'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', :class="[
]"> styles[props.status.type],
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" /> showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }} {{ 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
styles[props.status.type], :class="[
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group', styles[props.status.type],
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', 'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
]"> 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<ChevronDownIcon class="size-5" aria-hidden="true" /> <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
active @click="() => emit('options')"
? 'bg-zinc-800 text-zinc-100 outline-none' :class="[
: 'text-zinc-400', active
'w-full block px-4 py-2 text-sm inline-flex justify-between', ? 'bg-zinc-800 text-zinc-100 outline-none'
]"> : 'text-zinc-400',
Options 'w-full block px-4 py-2 text-sm inline-flex justify-between',
<Cog6ToothIcon class="size-5" /> ]"
</button> >
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button @click="() => emit('uninstall')" :class="[ <button
active @click="() => emit('uninstall')"
? 'bg-zinc-800 text-zinc-100 outline-none' :class="[
: 'text-zinc-400', active
'w-full block px-4 py-2 text-sm inline-flex justify-between', ? 'bg-zinc-800 text-zinc-100 outline-none'
]"> : 'text-zinc-400',
Uninstall 'w-full block px-4 py-2 text-sm inline-flex justify-between',
<TrashIcon class="size-5" /> ]"
</button> >
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@ -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>

View File

@ -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>

View File

@ -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">
@ -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",

View File

@ -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">&rarr;</span> <button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
>
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
</button>
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
Use a code &rarr;
</NuxtLink>
</span> </span>
</button> </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);

View File

@ -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" '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',
:key="nav.id" navIndex === currentNavigation
:class="[ ? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
'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.isInstalled.value
nav.index === currentNavigation
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200' ? 'text-zinc-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");
} }
}); });

View File

@ -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" />

View File

@ -0,0 +1,3 @@
import type { AppState } from "~/types";
export const useAppState = () => useState<AppState | undefined>("state");

View File

@ -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;

View File

@ -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) {

View File

@ -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"

View File

@ -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
View 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
View 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">&larr;</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>

View 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>

View File

@ -243,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" />
@ -256,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"
@ -368,6 +392,13 @@
</template> </template>
</ModalTemplate> </ModalTemplate>
<!--
Dear future DecDuck,
This v-if is necessary for Vue rendering reasons
(it tries to access the game version for not installed games)
You have already tried to remove it
Don't.
-->
<GameOptionsModal <GameOptionsModal
v-if="status.type === GameStatusEnum.Installed" v-if="status.type === GameStatusEnum.Installed"
v-model="configureModalOpen" v-model="configureModalOpen"
@ -503,13 +534,13 @@ async function installFlow() {
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;
} }
} }

16
main/pages/news/index.vue Normal file
View 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
View File

@ -0,0 +1,5 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"exclude": ["src-tauri/**/*"]
}

View File

@ -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

File diff suppressed because it is too large Load Diff

22
optimize-appimage.sh Executable file
View 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"

View File

@ -1,46 +1,22 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.3.0",
"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"
} }

342
src-tauri/Cargo.lock generated
View File

@ -345,6 +345,22 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "async-tungstenite"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
dependencies = [
"atomic-waker",
"futures-core",
"futures-io",
"futures-task",
"futures-util",
"log",
"pin-project-lite",
"tungstenite",
]
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.2" version = "0.18.2"
@ -882,7 +898,7 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -1268,7 +1284,7 @@ dependencies = [
[[package]] [[package]]
name = "drop-app" name = "drop-app"
version = "0.3.0" version = "0.3.2"
dependencies = [ dependencies = [
"atomic-instant-full", "atomic-instant-full",
"bitcode", "bitcode",
@ -1280,22 +1296,26 @@ dependencies = [
"droplet-rs", "droplet-rs",
"dynfmt", "dynfmt",
"filetime", "filetime",
"futures-lite",
"gethostname", "gethostname",
"hex 0.4.3", "hex 0.4.3",
"http 1.3.1", "http 1.3.1",
"http-serde 2.1.1", "http-serde 2.1.1",
"humansize",
"known-folders", "known-folders",
"log", "log",
"log4rs", "log4rs",
"md5", "md5",
"native_model", "native_model",
"page_size",
"parking_lot 0.12.3", "parking_lot 0.12.3",
"rand 0.9.1", "rand 0.9.1",
"rayon", "rayon",
"regex", "regex",
"reqwest 0.12.16", "reqwest 0.12.22",
"reqwest-middleware 0.4.2", "reqwest-middleware 0.4.2",
"reqwest-middleware-cache", "reqwest-middleware-cache",
"reqwest-websocket",
"rustbreak", "rustbreak",
"rustix 0.38.44", "rustix 0.38.44",
"schemars", "schemars",
@ -1305,6 +1325,7 @@ dependencies = [
"sha1", "sha1",
"shared_child", "shared_child",
"slice-deque", "slice-deque",
"sysinfo",
"tar", "tar",
"tauri", "tauri",
"tauri-build", "tauri-build",
@ -1567,6 +1588,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@ -1574,7 +1604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [ dependencies = [
"foreign-types-macros", "foreign-types-macros",
"foreign-types-shared", "foreign-types-shared 0.3.1",
] ]
[[package]] [[package]]
@ -1588,6 +1618,12 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "foreign-types-shared" name = "foreign-types-shared"
version = "0.3.1" version = "0.3.1"
@ -2276,6 +2312,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.2.0" version = "2.2.0"
@ -2336,6 +2381,7 @@ dependencies = [
"hyper 1.6.0", "hyper 1.6.0",
"hyper-util", "hyper-util",
"rustls", "rustls",
"rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -2343,6 +2389,22 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.13" version = "0.1.13"
@ -2755,9 +2817,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -2769,6 +2831,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.3" version = "0.1.3"
@ -3031,6 +3099,23 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework 2.11.1",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "native_model" name = "native_model"
version = "0.6.1" version = "0.6.1"
@ -3123,6 +3208,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@ -3319,6 +3413,16 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-io-kit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "objc2-io-surface" name = "objc2-io-surface"
version = "0.3.1" version = "0.3.1"
@ -3449,6 +3553,50 @@ dependencies = [
"pathdiff", "pathdiff",
] ]
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@ -3505,6 +3653,16 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@ -4262,9 +4420,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.16" version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf597b113be201cb2269b4c39b39a804d01b99ee95a4278f0ed04e45cff1c71" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@ -4277,22 +4435,23 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "native-tls",
"once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls", "rustls",
"rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
@ -4331,7 +4490,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"http 1.3.1", "http 1.3.1",
"reqwest 0.12.16", "reqwest 0.12.22",
"serde", "serde",
"thiserror 1.0.69", "thiserror 1.0.69",
"tower-service", "tower-service",
@ -4357,6 +4516,24 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "reqwest-websocket"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f91a811daaa8b54faeaec9d507a336897a3d243834a4965254a17d39da8b5c9"
dependencies = [
"async-tungstenite",
"bytes",
"futures-util",
"reqwest 0.12.22",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"tungstenite",
"web-sys",
]
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.15.3" version = "0.15.3"
@ -4511,6 +4688,18 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.2.0",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.12.0" version = "1.12.0"
@ -4553,6 +4742,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.22" version = "0.8.22"
@ -4586,6 +4784,42 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.24.0" version = "0.24.0"
@ -4955,7 +5189,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg_aliases", "cfg_aliases",
"core-graphics", "core-graphics",
"foreign-types", "foreign-types 0.5.0",
"js-sys", "js-sys",
"log", "log",
"objc2 0.5.2", "objc2 0.5.2",
@ -5143,6 +5377,20 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "sysinfo"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.5.1" version = "0.5.1"
@ -5255,9 +5503,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.6.2" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "124e129c9c0faa6bec792c5948c89e86c90094133b0b9044df0ce5f0a8efaa0d" checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -5282,7 +5530,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
"reqwest 0.12.16", "reqwest 0.12.22",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@ -5306,9 +5554,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.3.0" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83" checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -5328,9 +5576,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.3.0" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406" checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@ -5355,9 +5603,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.3.1" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc" checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -5537,9 +5785,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.7.0" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4" checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@ -5559,9 +5807,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.7.1" version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902b5aa9035e16f342eb64f8bf06ccdc2808e411a2525ed1d07672fa4e780bad" checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
dependencies = [ dependencies = [
"gtk", "gtk",
"http 1.3.1", "http 1.3.1",
@ -5586,9 +5834,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e" checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@ -5813,6 +6061,16 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.2" version = "0.26.2"
@ -5831,6 +6089,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-io",
"futures-sink", "futures-sink",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
@ -5916,9 +6175,9 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.4" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"bytes", "bytes",
@ -6009,6 +6268,23 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"
@ -6206,6 +6482,12 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.0"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.3.0" 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"
@ -68,6 +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"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
page_size = "0.6.0"
sysinfo = "0.36.1"
humansize = "2.1.3"
# tailscale = { path = "./tailscale" } # tailscale = { path = "./tailscale" }
[dependencies.dynfmt] [dependencies.dynfmt]
@ -75,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]
@ -99,9 +104,9 @@ 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", "blocking", "rustls-tls-webpki-roots"] features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"

View File

@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build();
} }

View File

@ -13,8 +13,8 @@ pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mut
let download_manager = state.lock().unwrap().download_manager.clone(); let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() { 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:?}"),
} }

View File

@ -7,7 +7,7 @@ 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::{
@ -59,6 +59,8 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
lock.applications.install_dirs.push(new_dir); lock.applications.install_dirs.push(new_dir);
drop(lock); drop(lock);
scan_install_dirs();
Ok(()) Ok(())
} }

View File

@ -10,7 +10,7 @@ use chrono::Utc;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use native_model::{Decode, Encode}; use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Serialize}; use serde::{Serialize, de::DeserializeOwned};
use url::Url; use url::Url;
use crate::DB; use crate::DB;
@ -67,20 +67,18 @@ 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()) { 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), Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
},
false => {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
} }
} else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
} }
} }
@ -124,14 +122,7 @@ fn handle_invalid_database(
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
@ -142,14 +133,21 @@ 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);
} }
match DB.save() { match DB.save() {
Ok(_) => {} Ok(()) => {}
Err(e) => { Err(e) => {
error!("database failed to save with error {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}")
@ -157,6 +155,7 @@ impl<'a> Drop for DBWrite<'a> {
} }
} }
} }
pub fn borrow_db_checked<'a>() -> DBRead<'a> { pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() { match DB.borrow_data() {
Ok(data) => DBRead(data), Ok(data) => DBRead(data),

View File

@ -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;

View File

@ -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,
} }
} }
} }

View 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,
);
}
}
}

View File

@ -4,12 +4,12 @@ use crate::{database::models::data::DownloadableMetadata, AppState};
#[tauri::command] #[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads() state.lock().unwrap().download_manager.pause_downloads();
} }
#[tauri::command] #[tauri::command]
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads() state.lock().unwrap().download_manager.resume_downloads();
} }
#[tauri::command] #[tauri::command]
@ -22,10 +22,10 @@ pub fn move_download_in_queue(
.lock() .lock()
.unwrap() .unwrap()
.download_manager .download_manager
.rearrange(old_index, new_index) .rearrange(old_index, new_index);
} }
#[tauri::command] #[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) { pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta) state.lock().unwrap().download_manager.cancel(meta);
} }

View File

@ -124,11 +124,16 @@ impl DownloadManagerBuilder {
self.current_download_agent = None; self.current_download_agent = None;
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); 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); drop(download_thread_lock);
} }
fn stop_and_wait_current_download(&self) { fn stop_and_wait_current_download(&self) -> bool {
self.set_status(DownloadManagerStatus::Paused); 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);
@ -136,8 +141,10 @@ impl DownloadManagerBuilder {
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); 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.join().unwrap(); return current_download_thread.join().is_ok();
} };
true
} }
fn manage_queue(mut self) -> Result<(), ()> { fn manage_queue(mut self) -> Result<(), ()> {
@ -176,7 +183,7 @@ impl DownloadManagerBuilder {
DownloadManagerSignal::Cancel(meta) => { DownloadManagerSignal::Cancel(meta) => {
self.manage_cancel_signal(&meta); self.manage_cancel_signal(&meta);
} }
}; }
} }
} }
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
@ -213,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;
} }
@ -254,13 +257,17 @@ impl DownloadManagerBuilder {
} }
}; };
// If the download gets cancel // If the download gets canceled
// immediately return, on_cancelled gets called for us earlier
if !download_result { if !download_result {
download_agent.on_incomplete(&app_handle);
return; return;
} }
let validate_result = match download_agent.validate() { if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
return;
}
let validate_result = match download_agent.validate(&app_handle) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
error!( error!(
@ -274,6 +281,10 @@ impl DownloadManagerBuilder {
} }
}; };
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
return;
}
if validate_result { if validate_result {
download_agent.on_complete(&app_handle); download_agent.on_complete(&app_handle);
sender sender
@ -316,6 +327,7 @@ impl DownloadManagerBuilder {
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata()); self.remove_and_cleanup_front_download(&current_agent.metadata());
} }
self.push_ui_queue_update();
self.set_status(DownloadManagerStatus::Error); self.set_status(DownloadManagerStatus::Error);
} }
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {

View File

@ -23,13 +23,13 @@ use super::{
}; };
pub enum DownloadManagerSignal { pub enum DownloadManagerSignal {
/// Resumes (or starts) the DownloadManager /// Resumes (or starts) the `DownloadManager`
Go, Go,
/// Pauses the DownloadManager /// Pauses the `DownloadManager`
Stop, Stop,
/// Called when a DownloadAgent has fully completed a download. /// Called when a `DownloadAgent` has fully completed a download.
Completed(DownloadableMetadata), Completed(DownloadableMetadata),
/// Generates and appends a DownloadAgent /// Generates and appends a `DownloadAgent`
/// to the registry and queue /// to the registry and queue
Queue(DownloadAgent), Queue(DownloadAgent),
/// Tells the Manager to stop the current /// Tells the Manager to stop the current
@ -70,14 +70,14 @@ pub enum DownloadStatus {
Error, Error,
} }
/// Accessible front-end for the DownloadManager /// Accessible front-end for the `DownloadManager`
/// ///
/// The system works entirely through signals, both internally and externally, /// The system works entirely through signals, both internally and externally,
/// all of which are accessible through the DownloadManagerSignal type, but /// all of which are accessible through the `DownloadManagerSignal` type, but
/// should not be used directly. Rather, signals are abstracted through this /// should not be used directly. Rather, signals are abstracted through this
/// interface. /// interface.
/// ///
/// The actual download queue may be accessed through the .edit() function, /// The actual download queue may be accessed through the .`edit()` function,
/// which provides raw access to the underlying queue. /// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!! /// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager { pub struct DownloadManager {
@ -139,7 +139,7 @@ impl DownloadManager {
pub fn rearrange(&self, current_index: usize, new_index: usize) { pub fn rearrange(&self, current_index: usize, new_index: usize) {
if current_index == new_index { if current_index == new_index {
return; return;
}; }
let needs_pause = current_index == 0 || new_index == 0; let needs_pause = current_index == 0 || new_index == 0;
if needs_pause { if needs_pause {
@ -183,7 +183,7 @@ impl DownloadManager {
} }
} }
/// Takes in the locked value from .edit() and attempts to /// Takes in the locked value from .`edit()` and attempts to
/// get the index of whatever id is passed in /// get the index of whatever id is passed in
fn get_index_from_id( fn get_index_from_id(
queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>, queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>,

View File

@ -14,14 +14,14 @@ use super::{
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>; fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl; fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus; fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata; fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle); fn on_initialised(&self, app_handle: &AppHandle);
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError); fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
fn on_complete(&self, app_handle: &AppHandle); fn on_complete(&self, app_handle: &AppHandle);
fn on_incomplete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle); fn on_cancelled(&self, app_handle: &AppHandle);
} }

View File

@ -22,10 +22,7 @@ impl From<DownloadThreadControlFlag> for bool {
/// false => Stop /// false => Stop
impl From<bool> for DownloadThreadControlFlag { impl From<bool> for DownloadThreadControlFlag {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
match value { if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
true => DownloadThreadControlFlag::Go,
false => DownloadThreadControlFlag::Stop,
}
} }
} }
@ -41,9 +38,9 @@ impl DownloadThreadControl {
} }
} }
pub fn get(&self) -> DownloadThreadControlFlag { pub fn get(&self) -> DownloadThreadControlFlag {
self.inner.load(Ordering::Relaxed).into() self.inner.load(Ordering::Acquire).into()
} }
pub fn set(&self, flag: DownloadThreadControlFlag) { pub fn set(&self, flag: DownloadThreadControlFlag) {
self.inner.store(flag.into(), Ordering::Relaxed); self.inner.store(flag.into(), Ordering::Release);
} }
} }

Some files were not shown because too many files have changed in this diff Show More