Compare commits

..

3 Commits

Author SHA1 Message Date
6dedf39420 bump version and update ci 2025-07-18 19:57:37 +10:00
79373cb0c1 fix: clippy 2025-07-18 19:38:33 +10:00
f264600619 fix: reverse proxy 400 due to duplicate header 2025-07-18 19:36:10 +10:00
136 changed files with 9237 additions and 10540 deletions

View File

@ -54,51 +54,16 @@ jobs:
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
# 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.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
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__'
releaseBody: 'See the assets to download this version and install. This release was created automatically.' releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
args: ${{ matrix.args }} args: ${{ matrix.args }}

5
.gitignore vendored
View File

@ -26,7 +26,4 @@ dist-ssr
.output .output
src-tauri/flamegraph.svg src-tauri/flamegraph.svg
src-tauri/perf* src-tauri/perf*
/*.AppImage
/squashfs-root

9
.gitmodules vendored
View File

@ -1,6 +1,9 @@
[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 "libs/drop-base"] [submodule "src-tauri/umu/umu-launcher"]
path = libs/drop-base path = src-tauri/umu/umu-launcher
url = https://github.com/drop-oss/drop-base.git url = https://github.com/Open-Wine-Components/umu-launcher.git

View File

@ -1,5 +1,4 @@
<template> <template>
<LoadingIndicator />
<NuxtLayout class="select-none w-screen h-screen"> <NuxtLayout class="select-none w-screen h-screen">
<NuxtPage /> <NuxtPage />
<ModalStack /> <ModalStack />
@ -10,6 +9,8 @@
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,
@ -19,26 +20,18 @@ 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);
}
async function fetchState() { router.beforeEach(async () => {
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,48 +0,0 @@
import fs from "fs";
import process from "process";
import childProcess from "child_process";
import createLogger from "pino";
const OUTPUT = "./.output";
const logger = createLogger({ transport: { target: "pino-pretty" } });
async function spawn(exec, opts) {
const output = childProcess.spawn(exec, { ...opts, shell: true });
output.stdout.on("data", (data) => {
process.stdout.write(data);
});
output.stderr.on("data", (data) => {
process.stderr.write(data);
});
return await new Promise((resolve, reject) => {
output.on("error", (err) => reject(err));
output.on("exit", () => resolve());
});
}
const 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,78 +1,52 @@
<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 <button type="button" @click="() => buttonActions[props.status.type]()" :class="[
type="button" styles[props.status.type],
@click="() => buttonActions[props.status.type]()" showDropdown ? 'rounded-l-md' : 'rounded-md',
:class="[ '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',
styles[props.status.type], ]">
showDropdown ? 'rounded-l-md' : 'rounded-md', <component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
'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 <Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full"> <div class="h-full">
<MenuButton <MenuButton :class="[
: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 <transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-active-class="transition ease-out duration-100" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
enter-from-class="transform opacity-0 scale-95" leave-from-class="transform opacity-100 scale-100" leave-to-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-if="showOptions" v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('options')" :class="[
@click="() => emit('options')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Options
]" <Cog6ToothIcon class="size-5" />
> </button>
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('uninstall')" :class="[
@click="() => emit('uninstall')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Uninstall
]" <TrashIcon class="size-5" />
> </button>
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@ -87,8 +61,6 @@ import {
ChevronDownIcon, ChevronDownIcon,
PlayIcon, PlayIcon,
QueueListIcon, QueueListIcon,
ServerIcon,
StopIcon,
WrenchIcon, WrenchIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
@ -105,7 +77,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(
@ -115,10 +87,6 @@ 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",
@ -126,8 +94,6 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "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]:
@ -139,45 +105,42 @@ 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-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500", "bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
}; };
const buttonNames: { [key in GameStatusEnum]: string } = { 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]: PlayIcon,
[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">
@ -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"
> >
<div class="flex-col gap-y-2"> <PanelWidget 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>
</div> </PanelWidget>
</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",

View File

@ -13,7 +13,11 @@
<div class="max-w-lg"> <div class="max-w-lg">
<slot /> <slot />
<div class="mt-10"> <div class="mt-10">
<div> <button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="text-sm text-left font-semibold leading-7 text-blue-600"
>
<div v-if="loading" role="status"> <div v-if="loading" role="status">
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -33,19 +37,10 @@
</svg> </svg>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
<span class="inline-flex gap-x-8 items-center" v-else> <span v-else>
<button Sign in with your browser <span aria-hidden="true">&rarr;</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>
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
Use a code &rarr;
</NuxtLink>
</span> </span>
</div> </button>
<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>
@ -126,7 +121,6 @@
<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,30 +1,19 @@
<template> <template>
<div> <div>
<div class="mb-3 inline-flex gap-x-2"> <div
class="relative mb-3 transition-transform duration-300 hover:scale-105 active:scale-95"
>
<div <div
class="relative transition-transform duration-300 hover:scale-105 active:scale-95" class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
> >
<div <MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<MagnifyingGlassIcon
class="h-5 w-5 text-zinc-400"
aria-hidden="true"
/>
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..."
/>
</div> </div>
<button <input
@click="() => calculateGames(true)" type="text"
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" 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"
<ArrowPathIcon class="size-4" /> placeholder="Search library..."
</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">
@ -71,7 +60,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid"; import { MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types"; import { GameStatusEnum, type Game, type GameStatus } from "~/types";
import { TransitionGroup } from "vue"; import { TransitionGroup } from "vue";
@ -81,26 +70,24 @@ import { listen } from "@tauri-apps/api/event";
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-blue-500",
[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-blue-500",
[GameStatusEnum.Updating]: "text-blue-500", [GameStatusEnum.Updating]: "text-blue-500",
[GameStatusEnum.Uninstalling]: "text-zinc-100", [GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500", [GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-400", [GameStatusEnum.PartiallyInstalled]: "text-gray-600"
}; };
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...",
[GameStatusEnum.SetupRequired]: "Setup required", [GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running", [GameStatusEnum.Running]: "Running",
[GameStatusEnum.PartiallyInstalled]: "Partially installed", [GameStatusEnum.PartiallyInstalled]: "Partially installed"
}; };
const router = useRouter(); const router = useRouter();
@ -114,20 +101,16 @@ 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() {
if (clearAll) rawGames.value = []; rawGames.value = await invoke("fetch_library");
// If we update immediately, the navigation gets re-rendered before we for (const game of rawGames.value) {
// add all the necessary state, and it freaks tf out
const newGames = await invoke<typeof rawGames.value>("fetch_library");
for (const game of newGames) {
if (games[game.id]) continue; if (games[game.id]) continue;
games[game.id] = await useGame(game.id); games[game.id] = await useGame(game.id);
} }
for (const game of newGames) { for (const game of rawGames.value) {
if (icons[game.id]) continue; if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId); icons[game.id] = await useObject(game.mIconObjectId);
} }
rawGames.value = newGames;
} }
await calculateGames(); await calculateGames();

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950" class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
> >
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()"> <div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
<Wordmark class="mt-1" /> <Wordmark class="mt-1" />

3
composables/app-state.ts Normal file
View File

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

View File

@ -14,6 +14,7 @@ export type SerializedGameStatus = [
]; ];
export const parseStatus = (status: SerializedGameStatus): GameStatus => { export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) { if (status[0]) {
return { return {
type: status[0].type, type: status[0].type,
@ -47,6 +48,7 @@ export const useGame = async (gameId: string) => {
status: SerializedGameStatus; status: SerializedGameStatus;
version?: GameVersion; version?: GameVersion;
} = event.payload as any; } = event.payload as any;
console.log(payload.status);
gameStatusRegistry[gameId].value = parseStatus(payload.status); gameStatusRegistry[gameId].value = parseStatus(payload.status);
/** /**

View File

@ -18,7 +18,7 @@ export function setupHooks() {
}); });
listen("auth/finished", async (event) => { listen("auth/finished", async (event) => {
router.push("/library"); router.push("/store");
state.value = JSON.parse(await invoke("fetch_state")); state.value = JSON.parse(await invoke("fetch_state"));
}); });
@ -30,31 +30,12 @@ export function setupHooks() {
description: `Drop encountered an error while downloading your game: "${( description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string event.payload as unknown as string
).toString()}"`, ).toString()}"`,
buttonText: "Close", buttonText: "Close"
}, },
(e, c) => c() (e, c) => c()
); );
}); });
// This is for errors that (we think) aren't our fault
listen("launch_external_error", (event) => {
createModal(
ModalType.Confirmation,
{
title: "Did something go wrong?",
description:
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
buttonText: "Open",
},
async (e, c) => {
if (e == "confirm") {
await invoke("open_process_logs", { gameId: event.payload });
}
c();
}
);
});
/* /*
document.addEventListener("contextmenu", (event) => { document.addEventListener("contextmenu", (event) => {
@ -65,13 +46,7 @@ export function setupHooks() {
*/ */
} }
export function initialNavigation(state: ReturnType<typeof useAppState>) { export function initialNavigation(state: Ref<AppState>) {
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) {
@ -88,6 +63,6 @@ export function initialNavigation(state: ReturnType<typeof useAppState>) {
router.push("/error/serverunavailable"); router.push("/error/serverunavailable");
break; break;
default: default:
router.push("/library"); router.push("/store");
} }
} }

1
drop-base Submodule

Submodule drop-base added at 26698e5b06

View File

@ -7,7 +7,6 @@
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8" 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"

Submodule libs/drop-base deleted from 04125e89be

View File

@ -1,7 +0,0 @@
<template></template>
<script setup lang="ts">
const loading = useLoadingIndicator();
watch(loading.isLoading, console.log);
</script>

View File

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

View File

@ -1,37 +0,0 @@
{
"name": "view",
"private": true,
"version": "0.3.1",
"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"
}

View File

@ -1,37 +0,0 @@
<template>
<div class="min-h-full w-full flex items-center justify-center">
<div class="flex flex-col items-center">
<div class="text-center">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Device authorization
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-md mx-auto">
Open Drop on another one of your devices, and use your account
dropdown to "Authorize client", and enter the code below.
</p>
<div
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
>
<span v-for="letter in code.split('')">{{ letter }}</span>
</div>
</div>
<div class="mt-10 flex items-center justify-center gap-x-6">
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
><span aria-hidden="true">&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

@ -1,37 +0,0 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<BuildingStorefrontIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Store not supported in client
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
Currently, Drop requires you to view the store in your browser.
Please click the button below to open it in your default browser.
</p>
<NuxtLink
:href="storeUrl"
target="_blank"
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
BuildingStorefrontIcon,
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
</script>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,5 @@ export default defineNuxtConfig({
ssr: false, ssr: false,
extends: [["../libs/drop-base"]], extends: [["./drop-base"]],
app: {
baseURL: "/main",
}
}); });

View File

@ -1,22 +0,0 @@
## This script is largely useless, because there's not much we can do about AppImage size
ARCH=$(uname -m)
# build tauri apps
# NO_STRIP=true yarn tauri build -- --verbose
# unpack appimage
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)
"$APPIMAGE" --appimage-extract
# strip binary
APPIMAGE_UNPACK="./squashfs-root"
find $APPIMAGE_UNPACK -type f -exec strip -s {} \;
APPIMAGETOOL=$(echo "obsolete-appimagetool-$ARCH.AppImage")
wget -O $APPIMAGETOOL "https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGETOOL"
chmod +x $APPIMAGETOOL
APPIMAGE_OUTPUT=$(./$APPIMAGETOOL $APPIMAGE_UNPACK | grep ".AppImage" | grep squashfs-root | awk '{ print $6 }')
mv $APPIMAGE_OUTPUT "$APPIMAGE"

View File

@ -1,22 +1,45 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.3.0-rc-7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node ./build.mjs", "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.7.0", "@headlessui/vue": "^1.7.23",
"@tauri-apps/plugin-deep-link": "^2.4.1", "@heroicons/vue": "^2.1.5",
"@tauri-apps/plugin-dialog": "^2.3.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/plugin-opener": "^2.4.0", "@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.0.1",
"pino": "^9.7.0", "@tauri-apps/plugin-os": "~2",
"pino-pretty": "^13.1.1" "@tauri-apps/plugin-shell": "^2.2.1",
"koa": "^2.16.1",
"markdown-it": "^14.1.0",
"micromark": "^4.0.1",
"nuxt": "^3.16.0",
"scss": "^0.2.4",
"vue": "latest",
"vue-router": "latest",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.7.1" "@tailwindcss/forms": "^0.5.9",
} "@tailwindcss/typography": "^0.5.15",
"@tauri-apps/cli": ">=2.0.0",
"@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -76,7 +76,7 @@
<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="index" :key="url"
:src="url" :src="url"
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"
@ -157,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">
@ -350,7 +350,9 @@
<template #buttons> <template #buttons>
<LoadingButton <LoadingButton
@click="() => install()" @click="() => install()"
:disabled="!(versionOptions && versionOptions.length > 0)" :disabled="
!(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"
@ -368,18 +370,7 @@
</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"
@ -429,7 +420,7 @@
<img <img
v-for="(url, index) in mediaUrls" v-for="(url, index) in mediaUrls"
v-show="currentImageIndex === index" v-show="currentImageIndex === index"
:key="index" :key="url"
: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}`"
@ -450,6 +441,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
Dialog,
DialogTitle,
TransitionChild,
TransitionRoot,
Listbox, Listbox,
ListboxButton, ListboxButton,
ListboxLabel, ListboxLabel,
@ -487,10 +482,7 @@ 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(async (v) => { game.value.mImageCarouselObjectIds.map((id) => useObject(id))
const src = await useObject(v);
return src;
})
); );
const htmlDescription = micromark(game.value.mDescription); const htmlDescription = micromark(game.value.mDescription);
@ -504,6 +496,7 @@ 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;
@ -542,11 +535,12 @@ 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) { }
console.error(e); catch(e) {
} console.error(e)
}
} }
async function launch() { async function launch() {

View File

@ -106,6 +106,8 @@ const systemData = await invoke<{
dataDir: string; dataDir: string;
}>("fetch_system_data"); }>("fetch_system_data");
console.log(systemData);
clientId.value = systemData.clientId; clientId.value = systemData.clientId;
baseUrl.value = systemData.baseUrl; baseUrl.value = systemData.baseUrl;
dataDir.value = systemData.dataDir; dataDir.value = systemData.dataDir;

3
pages/store/index.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
</template>
<script setup lang="ts"></script>

518
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.3.1" version = "0.3.0-rc-7"
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 = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -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"
serde-binary = "0.5.0"
rayon = "1.10.0" rayon = "1.10.0"
webbrowser = "1.0.2" webbrowser = "1.0.2"
url = "2.5.2" url = "2.5.2"
@ -66,13 +67,6 @@ filetime = "0.2.25"
walkdir = "2.5.0" walkdir = "2.5.0"
known-folders = "1.2.0" 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"
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]
@ -80,7 +74,7 @@ version = "0.1.5"
features = ["curly"] features = ["curly"]
[dependencies.tauri] [dependencies.tauri]
version = "2.7.0" version = "2.1.1"
features = ["protocol-asset", "tray-icon"] features = ["protocol-asset", "tray-icon"]
[dependencies.tokio] [dependencies.tokio]
@ -106,7 +100,7 @@ features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.12" version = "0.12"
default-features = false default-features = false
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"] features = ["json", "http2", "blocking", "rustls-tls-webpki-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

@ -14,7 +14,6 @@
"core:window:allow-close", "core:window:allow-close",
"deep-link:default", "deep-link:default",
"dialog:default", "dialog:default",
"os:default", "os:default"
"opener:default"
] ]
} }

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, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError, database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
}; };
use super::{ use super::{
@ -59,8 +59,6 @@ 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::{Serialize, de::DeserializeOwned}; use serde::{de::DeserializeOwned, Serialize};
use url::Url; use url::Url;
use crate::DB; use crate::DB;
@ -67,18 +67,20 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap();
if exists { match exists {
match PathDatabase::load_from_path(db_path.clone()) { true => 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")
} }
} }
@ -122,7 +124,14 @@ 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 = Database; type Target = RwLockWriteGuard<'a, 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
@ -133,21 +142,14 @@ impl<'a> DerefMut for DBWrite<'a> {
&mut self.0 &mut self.0
} }
} }
impl<'a> Deref for DBRead<'a> { impl<'a> Drop for DBWrite<'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}")
@ -155,7 +157,6 @@ impl Drop for DBWrite<'_> {
} }
} }
} }
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,4 +2,3 @@ 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,14 +1,8 @@
/** 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};
@ -24,14 +18,16 @@ 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; use std::{collections::HashMap, process::Command};
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::{Deserialize, Serialize, native_model}; use super::*;
fn default_template() -> String { fn default_template() -> String {
"{}".to_owned() "{}".to_owned()
@ -119,7 +115,6 @@ 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 {},
} }
@ -179,10 +174,7 @@ 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)]
@ -214,7 +206,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: None, compat_info: crate::database::models::Database::create_new_compat_info(),
} }
} }
} }
@ -291,10 +283,7 @@ 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 {
@ -308,7 +297,6 @@ 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 {
@ -318,13 +306,22 @@ 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: None, compat_info: Database::create_new_compat_info(),
} }
} }
} }
} }
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>,
@ -339,11 +336,11 @@ pub mod data {
transient_statuses: HashMap::new(), transient_statuses: HashMap::new(),
}, },
prev_database, prev_database,
base_url: String::new(), base_url: "".to_owned(),
auth: None, auth: None,
settings: Settings::default(), settings: Settings::default(),
cache_dir, cache_dir,
compat_info: None, compat_info: Database::create_new_compat_info(),
} }
} }
} }

View File

@ -1,52 +0,0 @@
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

@ -1,10 +1,10 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{ sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex, Arc, Mutex,
mpsc::{Receiver, Sender, channel},
}, },
thread::{JoinHandle, spawn}, thread::{spawn, JoinHandle},
}; };
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
@ -176,7 +176,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) {
@ -212,13 +212,13 @@ impl DownloadManagerBuilder {
if self.current_download_agent.is_some() if self.current_download_agent.is_some()
&& 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!( debug!(
"Current download agent: {:?}", "Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata() self.current_download_agent.as_ref().unwrap().metadata()
); );
return; return;
} }
debug!("current download queue: {:?}", self.download_queue.read()); debug!("current download queue: {:?}", self.download_queue.read());
@ -242,47 +242,43 @@ impl DownloadManagerBuilder {
let app_handle = self.app_handle.clone(); let app_handle = self.app_handle.clone();
*download_thread_lock = Some(spawn(move || { *download_thread_lock = Some(spawn(move || {
loop { match download_agent.download(&app_handle) {
let download_result = match download_agent.download(&app_handle) { // 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() {
error!("download {:?} has error {}", download_agent.metadata(), &e); Ok(true) => {
download_agent.on_error(&app_handle, &e); download_agent.on_complete(&app_handle);
sender.send(DownloadManagerSignal::Error(e)).unwrap(); sender
return; .send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
Ok(false) => {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
} }
};
// If the download gets cancelled
// immediately return, on_cancelled gets called for us earlier
if !download_result {
return;
} }
// Ok(false) is for incomplete but exited properly
let validate_result = match download_agent.validate(&app_handle) { Ok(false) => {
Ok(v) => v, debug!("Donwload agent finished incomplete");
Err(e) => { download_agent.on_incomplete(&app_handle);
error!( }
"download {:?} has validation error {}", Err(e) => {
download_agent.metadata(), error!("download {:?} has error {}", download_agent.metadata(), &e);
&e download_agent.on_error(&app_handle, &e);
); sender.send(DownloadManagerSignal::Error(e)).unwrap();
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
return;
}
};
if validate_result {
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
return;
} }
} }
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
})); }));
self.set_status(DownloadManagerStatus::Downloading); self.set_status(DownloadManagerStatus::Downloading);
@ -299,12 +295,11 @@ impl DownloadManagerBuilder {
} }
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 if interface.metadata() == meta {
{ self.remove_and_cleanup_front_download(&meta);
self.remove_and_cleanup_front_download(&meta); }
} }
self.push_ui_queue_update(); self.push_ui_queue_update();
self.sender.send(DownloadManagerSignal::Go).unwrap(); self.sender.send(DownloadManagerSignal::Go).unwrap();
} }

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 {
@ -148,7 +148,9 @@ impl DownloadManager {
.unwrap(); .unwrap();
} }
debug!("moving download at index {current_index} to index {new_index}"); debug!(
"moving download at index {current_index} to index {new_index}"
);
let mut queue = self.edit(); let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap(); let to_move = queue.remove(current_index).unwrap();
@ -183,7 +185,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

@ -1,5 +1,5 @@
pub mod commands; pub mod commands;
pub mod download_manager_builder;
pub mod download_manager_frontend; pub mod download_manager_frontend;
pub mod download_manager_builder;
pub mod downloadable; pub mod downloadable;
pub mod util; pub mod util;

View File

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

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