Compare commits
59 commits
main
...
generic-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a154d1ea11 | ||
|
|
cba7d8e523 | ||
|
|
02f8591a60 | ||
|
|
d58348434c | ||
|
|
4a5f416d95 | ||
|
|
a2bcf5325f | ||
|
|
6b029041ed | ||
|
|
5c12a29f66 | ||
|
|
df922f8580 | ||
|
|
d17ad6fcc9 | ||
|
|
9e7208edae | ||
|
|
bed2df49f8 | ||
|
|
a4a5839a3a | ||
|
|
352926af6c | ||
|
|
ca0fa8ffcb | ||
|
|
0a0d9d6294 | ||
|
|
adf9907dbf | ||
|
|
267a2beddf | ||
|
|
e7b1465478 | ||
|
|
8d46a94f90 | ||
|
|
8db2393346 | ||
|
|
61b3271559 | ||
|
|
5b5d6092fe | ||
|
|
972ebd6ab6 | ||
|
|
a17311a88d | ||
|
|
fb20c56d45 | ||
|
|
d9136b6ca8 | ||
|
|
472eb1d435 | ||
|
|
e91f79bf57 | ||
|
|
22f6d3083f | ||
|
|
6b96e408b2 | ||
|
|
c3f62222fe | ||
|
|
01e6162527 | ||
|
|
88b2505e71 | ||
|
|
95f2174f8d | ||
|
|
7c90d2b8fd | ||
|
|
d7b0302bdd | ||
|
|
3ccd44466f | ||
|
|
93b8b83c20 | ||
|
|
1861659daa | ||
|
|
327628b780 | ||
|
|
f4ac1c87cd | ||
|
|
03fa3646fa | ||
|
|
a881d8e248 | ||
|
|
dcb2c0f004 | ||
|
|
c722a54132 | ||
|
|
e72662c4a8 | ||
|
|
139bc0ca36 | ||
|
|
949acfc161 | ||
|
|
9af0d08875 | ||
|
|
dcb1564568 | ||
|
|
1f899ec349 | ||
|
|
6a8d0af87d | ||
|
|
21835858f1 | ||
|
|
a135b1321c | ||
|
|
ad92dbec08 | ||
|
|
85a08990c3 | ||
|
|
dd7f5675d8 | ||
|
|
9ea2aa4997 |
67 changed files with 3085 additions and 1565 deletions
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
23
|
||||
15
README.md
15
README.md
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
||||
|
||||
## Running
|
||||
Before setting up the drop app, be sure that you have a server set up.
|
||||
The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
|
||||
|
||||
## Current features
|
||||
Currently supported are the following features:
|
||||
- Signin (with custom server)
|
||||
- Database registering & recovery
|
||||
- Dynamic library fetching from server
|
||||
- Installing & uninstalling games
|
||||
- Download progress monitoring
|
||||
- Launching / playing games
|
||||
|
||||
## Development
|
||||
|
||||
Install dependencies with `yarn`
|
||||
|
|
@ -10,7 +23,7 @@ Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use she
|
|||
|
||||
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
|
||||
|
||||
e.g. `RUST_LOG=debug yarn taudi dev`
|
||||
e.g. `RUST_LOG=debug yarn tauri dev`
|
||||
|
||||
## Contributing
|
||||
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.
|
||||
6
app.vue
6
app.vue
|
|
@ -6,7 +6,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "~/composables/queue";
|
||||
import "~/composables/downloads.js";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AppStatus } from "~/types";
|
||||
|
|
@ -20,10 +20,10 @@ import {
|
|||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
state.value = await invoke("fetch_state");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
|
||||
router.beforeEach(async () => {
|
||||
state.value = await invoke("fetch_state");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
|
|
|||
|
|
@ -60,3 +60,25 @@ $helvetica: (
|
|||
src: url("/fonts/inter/InterVariable-Italic.ttf");
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: 4px;
|
||||
scrollbar-color: #52525b #00000000;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #52525b;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #52525b;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,76 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex uppercase font-display items-center gap-x-2 rounded-md px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
||||
<div class="h-full">
|
||||
<MenuButton :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm'
|
||||
]">
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<MenuItems
|
||||
class="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button @click="() => emit('uninstall')"
|
||||
:class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']">Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
TrashIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
|
||||
import type { Component } from "vue";
|
||||
import { GameStatusEnum, type GameStatus } from "~/types.js";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
|
||||
const props = defineProps<{ status: GameStatus }>();
|
||||
const emit = defineEmits<{
|
||||
(e: "install"): void;
|
||||
(e: "play"): void;
|
||||
(e: "launch"): void;
|
||||
(e: "queue"): void;
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
|
||||
[GameStatusEnum.Queued]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Downloading]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]:
|
||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
|
||||
[GameStatusEnum.Installed]:
|
||||
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
|
||||
[GameStatusEnum.Updating]: "",
|
||||
[GameStatusEnum.Uninstalling]: "",
|
||||
[GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
|
||||
[GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
|
||||
[GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
|
||||
[GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
|
||||
[GameStatusEnum.Running]: "bg-zinc-800 text-white focus-visible:outline-zinc-700"
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
|
|
@ -57,6 +81,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
|
|||
[GameStatusEnum.Installed]: "Play",
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Stop"
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
|
|
@ -67,15 +92,17 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
|||
[GameStatusEnum.Installed]: PlayIcon,
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: PlayIcon
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.Remote]: () => emit("install"),
|
||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||
[GameStatusEnum.SetupRequired]: () => {},
|
||||
[GameStatusEnum.Installed]: () => emit("play"),
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Uninstalling]: () => { },
|
||||
[GameStatusEnum.Running]: () => emit("kill")
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,31 @@
|
|||
</span>
|
||||
</button>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
You can manually enter the token from your web browser.
|
||||
</p>
|
||||
<div class="inline-flex gap-x-1 mt-2 w-full">
|
||||
<input
|
||||
id="token"
|
||||
name="token"
|
||||
type="text"
|
||||
autocomplete="token"
|
||||
required
|
||||
class="grow block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
v-model="manualToken"
|
||||
/>
|
||||
<LoadingButton
|
||||
:loading="manualLoading"
|
||||
@click="() => continueManual_wrapper()"
|
||||
class="w-fit"
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-5 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
|
|
@ -101,6 +126,10 @@ import { invoke } from "@tauri-apps/api/core";
|
|||
const loading = ref(false);
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
const offerManual = ref(false);
|
||||
const manualToken = ref("");
|
||||
const manualLoading = ref(false);
|
||||
|
||||
async function auth() {
|
||||
await invoke("auth_initiate");
|
||||
}
|
||||
|
|
@ -111,5 +140,23 @@ function authWrapper_wrapper() {
|
|||
loading.value = false;
|
||||
error.value = e;
|
||||
});
|
||||
setTimeout(() => {
|
||||
offerManual.value = true;
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async function continueManual() {
|
||||
await invoke("manual_recieve_handshake", { token: manualToken.value });
|
||||
}
|
||||
|
||||
function continueManual_wrapper() {
|
||||
loading.value = true;
|
||||
continueManual()
|
||||
.catch((e) => {
|
||||
error.value = e;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
7
components/PageWidget.vue
Normal file
7
components/PageWidget.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<NuxtLink
|
||||
class="inline-flex items-center gap-x-2 px-1 py-0.5 rounded bg-blue-900 text-zinc-100 hover:bg-blue-800"
|
||||
>
|
||||
<slot />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
28
composables/downloads.ts
Normal file
28
composables/downloads.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
import type { DownloadableMetadata } from "~/types";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type StatsState = {
|
||||
speed: number; // Bytes per second
|
||||
time: number; // Seconds,
|
||||
};
|
||||
|
||||
export const useQueueState = () =>
|
||||
useState<QueueState>("queue", () => ({ queue: [], status: "Unknown" }));
|
||||
|
||||
export const useStatsState = () =>
|
||||
useState<StatsState>("stats", () => ({ speed: 0, time: 0 }));
|
||||
|
||||
listen("update_queue", (event) => {
|
||||
const queue = useQueueState();
|
||||
queue.value = event.payload as QueueState;
|
||||
});
|
||||
|
||||
listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ export type SerializedGameStatus = [
|
|||
];
|
||||
|
||||
const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
console.log(status);
|
||||
if (status[0]) {
|
||||
return {
|
||||
type: status[0].type,
|
||||
|
|
@ -28,28 +29,29 @@ const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
|||
}
|
||||
};
|
||||
|
||||
export const useGame = async (id: string) => {
|
||||
if (!gameRegistry[id]) {
|
||||
export const useGame = async (gameId: string) => {
|
||||
if (!gameRegistry[gameId]) {
|
||||
const data: { game: Game; status: SerializedGameStatus } = await invoke(
|
||||
"fetch_game",
|
||||
{
|
||||
id,
|
||||
gameId,
|
||||
}
|
||||
);
|
||||
gameRegistry[id] = data.game;
|
||||
if (!gameStatusRegistry[id]) {
|
||||
gameStatusRegistry[id] = ref(parseStatus(data.status));
|
||||
gameRegistry[gameId] = data.game;
|
||||
if (!gameStatusRegistry[gameId]) {
|
||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||
|
||||
listen(`update_game/${id}`, (event) => {
|
||||
listen(`update_game/${gameId}`, (event) => {
|
||||
const payload: {
|
||||
status: SerializedGameStatus;
|
||||
} = event.payload as any;
|
||||
gameStatusRegistry[id].value = parseStatus(payload.status);
|
||||
console.log(payload.status);
|
||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const game = gameRegistry[id];
|
||||
const status = gameStatusRegistry[id];
|
||||
const game = gameRegistry[gameId];
|
||||
const status = gameStatusRegistry[gameId];
|
||||
return { game, status };
|
||||
};
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export type QueueState = {
|
||||
queue: Array<{ id: string; status: string, progress: number | null }>;
|
||||
};
|
||||
|
||||
export const useQueueState = () =>
|
||||
useState<QueueState>("queue", () => ({ queue: [] }));
|
||||
|
||||
listen("update_queue", (event) => {
|
||||
const queue = useQueueState();
|
||||
queue.value = event.payload as QueueState;
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
import { data } from "autoprefixer";
|
||||
import { AppStatus, type AppState } from "~/types";
|
||||
|
||||
export function setupHooks() {
|
||||
|
|
@ -18,6 +19,20 @@ export function setupHooks() {
|
|||
router.push("/store");
|
||||
});
|
||||
|
||||
listen("download_error", (event) => {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Drop encountered an error while downloading",
|
||||
description: `Drop encountered an error while downloading your game: "${(
|
||||
event.payload as unknown as string
|
||||
).toString()}"`,
|
||||
buttonText: "Close"
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@
|
|||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^3.13.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue": "latest",
|
||||
|
|
|
|||
72
pages/account.vue
Normal file
72
pages/account.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="mx-auto max-w-7xl px-8">
|
||||
<div class="border-b border-zinc-700 py-5">
|
||||
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||
Account
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="divide-y divide-zinc-700">
|
||||
<div class="py-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Sign out of your Drop account on this device
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="signOut"
|
||||
type="button"
|
||||
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useRouter } from '#imports'
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
|
||||
const router = useRouter()
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Listen for auth events
|
||||
onMounted(async () => {
|
||||
await listen('auth/signedout', () => {
|
||||
router.push('/auth/signedout')
|
||||
})
|
||||
})
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
error.value = null
|
||||
await invoke('sign_out')
|
||||
} catch (e) {
|
||||
error.value = `Failed to sign out: ${e}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -6,29 +6,22 @@
|
|||
v-for="(nav, navIdx) in navigation"
|
||||
:key="nav.route"
|
||||
:class="[
|
||||
'transition group rounded flex justify-between gap-x-6 py-2 px-3',
|
||||
navIdx === currentNavigationIndex ? 'bg-zinc-900' : '',
|
||||
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
|
||||
navIdx === currentNavigationIndex
|
||||
? 'bg-zinc-800 text-zinc-100'
|
||||
: 'bg-zinc-900/50 text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="nav.route"
|
||||
>
|
||||
<div class="flex items-center min-w-0 gap-x-2">
|
||||
<div class="flex items-center w-full gap-x-3">
|
||||
<img
|
||||
class="h-5 w-auto flex-none object-cover rounded-sm bg-zinc-900"
|
||||
class="h-8 w-10 flex-none object-cover bg-zinc-900"
|
||||
:src="icons[navIdx]"
|
||||
alt=""
|
||||
/>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p
|
||||
:class="[
|
||||
navIdx === currentNavigationIndex
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-300',
|
||||
'truncate transition text-sm font-display leading-6',
|
||||
]"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="truncate text-sm font-display leading-6 flex-1">
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@
|
|||
<div class="h-full flex flex-row gap-x-4 items-stretch">
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@play="() => play()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
|
|
@ -38,285 +40,222 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<TransitionRoot as="template" :show="installFlowOpen">
|
||||
<Dialog class="relative z-50" @close="installFlowOpen = false">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity"
|
||||
/>
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-start justify-center p-4 text-center sm:items-center sm:p-0"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<form
|
||||
@submit.prevent="() => install()"
|
||||
class="relative transform rounded-lg bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
|
||||
>
|
||||
<div class="px-4 pb-4 pt-5 space-y-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Drop will add {{ game.mName }} to the queue to be
|
||||
downloaded. While downloading, Drop may use up a large
|
||||
amount of resources, particularly network bandwidth and
|
||||
CPU utilisation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div v-if="versionOptions && versionOptions.length > 0">
|
||||
<Listbox as="div" v-model="installVersionIndex">
|
||||
<ListboxLabel
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Version</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate"
|
||||
>{{
|
||||
versionOptions[installVersionIndex].versionName
|
||||
}}
|
||||
on
|
||||
{{
|
||||
versionOptions[installVersionIndex].platform
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(version, versionIdx) in versionOptions"
|
||||
:key="version.versionName"
|
||||
:value="versionIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version.versionName }} on
|
||||
{{ version.platform }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon
|
||||
class="h-5 w-5 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
There are no supported versions to install. Please
|
||||
contact your server admin or try again later.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Install to</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate">{{
|
||||
installDirs[installDir]
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(dir, dirIdx) in installDirs"
|
||||
:key="dir"
|
||||
:value="dirIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ dir }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="installError"
|
||||
class="mt-1 rounded-md bg-red-600/10 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon
|
||||
class="h-5 w-5 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ installError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-b-lg bg-zinc-800 px-4 py-3 sm:flex sm:gap-x-2 sm:flex-row-reverse sm:px-6"
|
||||
>
|
||||
<LoadingButton
|
||||
:disabled="
|
||||
!(
|
||||
versionOptions &&
|
||||
versionOptions.length > 0 &&
|
||||
!installDir
|
||||
)
|
||||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="installFlowOpen = false"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</TransitionChild>
|
||||
<ModalTemplate v-model="installFlowOpen">
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Drop will add {{ game.mName }} to the queue to be downloaded.
|
||||
While downloading, Drop may use up a large amount of resources,
|
||||
particularly network bandwidth and CPU utilisation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
|
||||
<form class="space-y-6">
|
||||
<div v-if="versionOptions && versionOptions.length > 0">
|
||||
<Listbox as="div" v-model="installVersionIndex">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Version</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate"
|
||||
>{{ versionOptions[installVersionIndex].versionName }}
|
||||
on
|
||||
{{ versionOptions[installVersionIndex].platform }}</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(version, versionIdx) in versionOptions"
|
||||
:key="version.versionName"
|
||||
:value="versionIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version.versionName }} on
|
||||
{{ version.platform }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
There are no supported versions to install. Please contact your
|
||||
server admin or try again later.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Install to</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate">{{
|
||||
installDirs[installDir]
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(dir, dirIdx) in installDirs"
|
||||
:key="dir"
|
||||
:value="dirIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ dir }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="text-zinc-400 text-sm mt-2">
|
||||
Add more install directories in
|
||||
<PageWidget to="/settings/downloads">
|
||||
<WrenchIcon class="size-3" />
|
||||
Settings
|
||||
</PageWidget>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ installError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="
|
||||
!(versionOptions && versionOptions.length > 0 && !installDir)
|
||||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="installFlowOpen = false"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -331,7 +270,11 @@ import {
|
|||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
|
@ -356,6 +299,8 @@ const versionOptions = ref<
|
|||
const installDirs = ref<undefined | Array<string>>();
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
installDirs.value = undefined;
|
||||
|
||||
try {
|
||||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||
|
|
@ -389,9 +334,9 @@ async function install() {
|
|||
}
|
||||
}
|
||||
|
||||
async function play() {
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { gameId: game.value.id });
|
||||
await invoke("launch_game", { id: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
|
|
@ -409,4 +354,25 @@ async function play() {
|
|||
async function queue() {
|
||||
router.push("/queue");
|
||||
}
|
||||
|
||||
async function uninstall() {
|
||||
await invoke("uninstall_game", { gameId: game.value.id });
|
||||
}
|
||||
|
||||
async function kill() {
|
||||
try {
|
||||
await invoke("kill_game", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: `Couldn't stop "${game.value.mName}"`,
|
||||
description: `Drop failed to stop "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
143
pages/queue.vue
143
pages/queue.vue
|
|
@ -1,27 +1,32 @@
|
|||
<template>
|
||||
<div class="bg-zinc-950 p-4 min-h-full">
|
||||
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
||||
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
|
||||
<div
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span>
|
||||
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
|
||||
|
||||
</div>
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }"
|
||||
class="w-[8px] bg-blue-600/40" />
|
||||
</div>
|
||||
</div>
|
||||
<draggable v-model="queue.queue" @end="onEnd">
|
||||
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
||||
<li
|
||||
v-if="games[element.id]"
|
||||
:key="element.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
|
||||
>
|
||||
<li v-if="games[element.meta.id]" :key="element.meta.id"
|
||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
|
||||
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
||||
<img
|
||||
class="size-24 flex-none bg-zinc-800 object-cover rounded"
|
||||
:src="games[element.id].cover"
|
||||
alt=""
|
||||
/>
|
||||
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" />
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-xl font-semibold text-zinc-100">
|
||||
<NuxtLink :href="`/library/${element.id}`" class="">
|
||||
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
||||
<span class="absolute inset-x-0 -top-px bottom-0" />
|
||||
{{ games[element.id].game.mName }}
|
||||
{{ games[element.meta.id].game.mName }}
|
||||
</NuxtLink>
|
||||
</p>
|
||||
<p class="mt-1 flex text-xs/5 text-gray-500">
|
||||
{{ games[element.id].game.mShortDescription }}
|
||||
{{ games[element.meta.id].game.mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,31 +35,20 @@
|
|||
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
||||
{{ element.status }}
|
||||
</p>
|
||||
<div
|
||||
v-if="element.progress"
|
||||
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-2 bg-blue-600"
|
||||
:style="{ width: `${element.progress * 100}%` }"
|
||||
/>
|
||||
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
|
||||
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<button @click="() => cancelGame(element.id)" class="group">
|
||||
<XMarkIcon
|
||||
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<button @click="() => cancelGame(element.meta)" class="group">
|
||||
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<p v-else>Loading...</p>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
|
||||
v-if="queue.queue.length == 0"
|
||||
>
|
||||
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
|
||||
No items in the queue
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -63,23 +57,68 @@
|
|||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Game, GameStatus } from "~/types";
|
||||
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
|
||||
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
window.addEventListener('resize', (event) => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
})
|
||||
|
||||
const queue = useQueueState();
|
||||
|
||||
const current = computed(() => queue.value.queue.at(0));
|
||||
const rest = computed(() => queue.value.queue.slice(1));
|
||||
const stats = useStatsState();
|
||||
const speedHistory = useState<Array<number>>(() => []);
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3);
|
||||
const previousGameId = ref<string | undefined>();
|
||||
|
||||
const games: Ref<{
|
||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||
}> = ref({});
|
||||
|
||||
|
||||
function resetHistoryGraph() {
|
||||
speedHistory.value = [];
|
||||
stats.value = { time: 0, speed: 0 };
|
||||
}
|
||||
function checkReset(v: QueueState) {
|
||||
const currentGame = v.queue.at(0)?.meta.id;
|
||||
// If we're finished
|
||||
if (!currentGame && previousGameId.value) {
|
||||
previousGameId.value = undefined;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
// If we started a new download
|
||||
if (currentGame && !previousGameId.value) {
|
||||
previousGameId.value = currentGame;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If it's a different game now
|
||||
if (currentGame != previousGameId.value
|
||||
) {
|
||||
previousGameId.value = currentGame;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
}
|
||||
watch(queue, (v) => {
|
||||
loadGamesForQueue(v);
|
||||
checkReset(v);
|
||||
});
|
||||
|
||||
watch(stats, (v) => {
|
||||
const newLength = speedHistory.value.push(v.speed);
|
||||
if (newLength > speedHistoryMax.value) {
|
||||
speedHistory.value.splice(0, 1);
|
||||
}
|
||||
checkReset(queue.value);
|
||||
})
|
||||
|
||||
function loadGamesForQueue(v: typeof queue.value) {
|
||||
for (const { id } of v.queue) {
|
||||
for (const { meta: { id } } of v.queue) {
|
||||
if (games.value[id]) return;
|
||||
(async () => {
|
||||
const gameData = await useGame(id);
|
||||
|
|
@ -98,7 +137,35 @@ async function onEnd(event: { oldIndex: number; newIndex: number }) {
|
|||
});
|
||||
}
|
||||
|
||||
async function cancelGame(id: string) {
|
||||
await invoke("cancel_game", { gameId: id });
|
||||
async function cancelGame(meta: DownloadableMetadata) {
|
||||
await invoke("cancel_game", { meta });
|
||||
}
|
||||
|
||||
function formatKilobytes(bytes: number): string {
|
||||
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
const scalar = 1000;
|
||||
|
||||
while (value >= scalar && unitIndex < units.length - 1) {
|
||||
value /= scalar;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return `${Math.round(seconds)}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,25 +9,18 @@
|
|||
<nav class="flex flex-col" aria-label="Sidebar">
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
|
||||
<NuxtLink
|
||||
:href="item.route"
|
||||
:class="[
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentPageIndex
|
||||
? 'bg-zinc-800/50 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
||||
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||
]">
|
||||
<component :is="item.icon" :class="[
|
||||
itemIdx === currentPageIndex
|
||||
? 'bg-zinc-800/50 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
||||
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
:class="[
|
||||
itemIdx === currentPageIndex
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
]" aria-hidden="true" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
|
|
@ -43,11 +36,14 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CubeIcon,
|
||||
HomeIcon,
|
||||
RectangleGroupIcon,
|
||||
BugAntIcon,
|
||||
} from "@heroicons/vue/16/solid";
|
||||
import type { Component } from "vue";
|
||||
import type { NavigationItem } from "~/types";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{
|
||||
|
|
@ -68,7 +64,20 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
|||
prefix: "/settings/downloads",
|
||||
icon: ArrowDownTrayIcon,
|
||||
},
|
||||
{
|
||||
label: "Debug Info",
|
||||
route: "/settings/debug",
|
||||
prefix: "/settings/debug",
|
||||
icon: BugAntIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const currentPlatform = platform();
|
||||
switch (currentPlatform) {
|
||||
case "linux":
|
||||
navigation.splice(2, 0, { label: "Compatibility", route: "/settings/compatibility", prefix: "/settings/compatibility", icon: CubeIcon });
|
||||
break;
|
||||
}
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
</script>
|
||||
|
|
|
|||
3
pages/settings/compatibility.vue
Normal file
3
pages/settings/compatibility.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
161
pages/settings/debug.vue
Normal file
161
pages/settings/debug.vue
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<div class="divide-y divide-zinc-700">
|
||||
<div class="py-6">
|
||||
<h2 class="text-base font-semibold font-display leading-7 text-zinc-100">
|
||||
Debug Information
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Technical information about your Drop client installation, helpful for
|
||||
debugging.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 space-y-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<FingerPrintIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Client ID
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400 font-mono ml-8">
|
||||
{{ clientId || "Not signed in" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ComputerDesktopIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Platform
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400 font-mono ml-8">
|
||||
{{ platformInfo }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ServerIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Server URL
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400 font-mono ml-8">
|
||||
{{ baseUrl || "Not connected" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<FolderIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Data Directory
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400 font-mono ml-8">
|
||||
{{ dataDir || "Unknown" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="compatInfo">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<CubeIcon class="h-5 w-5 text-zinc-400" />
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||
Compatibility Settings
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-2 ml-8 space-y-1">
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Enabled: {{ compatInfo.enabled ? "Yes" : "No" }}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Runner: {{ compatInfo.runner || "Not configured" }}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-400 font-mono">
|
||||
Prefix: {{ compatInfo.prefix || "Not configured" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex gap-x-4">
|
||||
<button
|
||||
@click="() => openDataDir()"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<FolderIcon class="h-5 w-5" aria-hidden="true" />
|
||||
Open Data Directory
|
||||
</button>
|
||||
<button
|
||||
@click="() => openLogFile()"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<DocumentTextIcon class="h-5 w-5" aria-hidden="true" />
|
||||
Open Log File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { platform, type } from "@tauri-apps/plugin-os";
|
||||
import {
|
||||
FingerPrintIcon,
|
||||
TagIcon,
|
||||
ComputerDesktopIcon,
|
||||
ServerIcon,
|
||||
FolderIcon,
|
||||
CubeIcon,
|
||||
DocumentTextIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
|
||||
const clientId = ref<string | null>(null);
|
||||
const platformInfo = ref("Loading...");
|
||||
const baseUrl = ref<string | null>(null);
|
||||
const dataDir = ref<string | null>(null);
|
||||
const compatInfo = ref<{
|
||||
enabled: boolean;
|
||||
runner: string | null;
|
||||
prefix: string | null;
|
||||
} | null>(null);
|
||||
|
||||
const systemData = await invoke<{
|
||||
clientId: string;
|
||||
baseUrl: string;
|
||||
dataDir: string;
|
||||
}>("fetch_system_data");
|
||||
|
||||
console.log(systemData);
|
||||
|
||||
clientId.value = systemData.clientId;
|
||||
baseUrl.value = systemData.baseUrl;
|
||||
dataDir.value = systemData.dataDir;
|
||||
|
||||
const currentPlatform = await platform();
|
||||
platformInfo.value = currentPlatform;
|
||||
|
||||
async function openDataDir() {
|
||||
if (!dataDir.value) return;
|
||||
try {
|
||||
await open(dataDir.value);
|
||||
} catch (error) {
|
||||
console.error("Failed to open data dir:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openLogFile() {
|
||||
if (!dataDir.value) return;
|
||||
try {
|
||||
const logPath = `${dataDir.value}/drop.log`;
|
||||
await open(logPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to open log file:", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,3 +1,60 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
<div class="divide-y divide-zinc-700">
|
||||
<div class="py-6">
|
||||
<h2 class="text-base font-semibold font-display leading-7 text-zinc-100">General</h2>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Configure basic application settings
|
||||
</p>
|
||||
|
||||
<div class="mt-10 space-y-8">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">Start with system</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Drop will automatically start when you log into your computer
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
v-model="autostartEnabled"
|
||||
:class="[
|
||||
autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
autostartEnabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
defineProps<{}>()
|
||||
|
||||
const autostartEnabled = ref<boolean>(false)
|
||||
|
||||
// Load initial state
|
||||
invoke('get_autostart_enabled').then((enabled) => {
|
||||
autostartEnabled.value = enabled as boolean
|
||||
})
|
||||
|
||||
// Watch for changes and update autostart
|
||||
watch(autostartEnabled, async (newValue: boolean) => {
|
||||
try {
|
||||
await invoke('toggle_autostart', { enabled: newValue })
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle autostart:', error)
|
||||
// Revert the toggle if it failed
|
||||
autostartEnabled.value = !newValue
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
|
|
|||
140
src-tauri/Cargo.lock
generated
140
src-tauri/Cargo.lock
generated
|
|
@ -261,6 +261,17 @@ version = "1.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auto-launch"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"thiserror 1.0.69",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
|
|
@ -355,6 +366,12 @@ dependencies = [
|
|||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "7.0.0"
|
||||
|
|
@ -864,7 +881,16 @@ version = "5.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -873,7 +899,18 @@ version = "5.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -965,6 +1002,7 @@ dependencies = [
|
|||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"chrono",
|
||||
"directories",
|
||||
"hex",
|
||||
|
|
@ -980,13 +1018,18 @@ dependencies = [
|
|||
"serde",
|
||||
"serde-binary",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"shared_child",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
"umu-wrapper-lib",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
|
|
@ -1037,7 +1080,7 @@ dependencies = [
|
|||
"rustc_version",
|
||||
"toml 0.8.2",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1435,6 +1478,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
|
|
@ -2853,6 +2906,17 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.1"
|
||||
|
|
@ -3793,9 +3857,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.11.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
|
@ -3811,9 +3875,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.11.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
|
|
@ -4107,6 +4171,15 @@ dependencies = [
|
|||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
|
|
@ -4205,7 +4278,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
"futures-util",
|
||||
|
|
@ -4255,7 +4328,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"glob",
|
||||
"heck 0.5.0",
|
||||
"json-patch",
|
||||
|
|
@ -4327,6 +4400,20 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9c13f843e5e5df3eed270fc42b02923cc1a6b5c7e56b0f3ac1d858ab2c8b5fb"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.2.0"
|
||||
|
|
@ -4388,6 +4475,24 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialize-to-javascript",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.2.0"
|
||||
|
|
@ -4813,7 +4918,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b"
|
|||
dependencies = [
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2",
|
||||
|
|
@ -4870,6 +4975,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "umu-wrapper-lib"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baa36636bef667cee9eb4f497c24279182b8b9f098fd04b0b8c5d2ebc4e451f1"
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
|
|
@ -5713,6 +5824,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
|
@ -26,7 +26,6 @@ tauri-build = { version = "2.0.0", features = [] }
|
|||
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
serde-binary = "0.5.0"
|
||||
rayon = "1.10.0"
|
||||
|
|
@ -41,12 +40,16 @@ http = "1.1.0"
|
|||
urlencoding = "2.1.3"
|
||||
md5 = "0.7.0"
|
||||
chrono = "0.4.38"
|
||||
tauri-plugin-os = "2"
|
||||
boxcar = "0.2.7"
|
||||
umu-wrapper-lib = "0.1.0"
|
||||
tauri-plugin-autostart = "2.0.0"
|
||||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
features = [
|
||||
"tray-icon"
|
||||
]
|
||||
features = ["tray-icon"]
|
||||
|
||||
|
||||
[dependencies.tokio]
|
||||
|
|
@ -81,6 +84,10 @@ features = [] # You can also use "yaml_enc" or "bin_enc"
|
|||
version = "0.12"
|
||||
features = ["json", "blocking"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive", "rc"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"core:window:allow-maximize",
|
||||
"core:window:allow-close",
|
||||
"deep-link:default",
|
||||
"dialog:default"
|
||||
"dialog:default",
|
||||
"os:default"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
use std::{
|
||||
env,
|
||||
sync::Mutex,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{info, warn};
|
||||
|
|
@ -13,7 +9,7 @@ use url::Url;
|
|||
|
||||
use crate::{
|
||||
db::{DatabaseAuth, DatabaseImpls},
|
||||
remote::RemoteAccessError,
|
||||
remote::{DropServerError, RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
|
||||
|
|
@ -78,7 +74,13 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
|||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
info!("Could not fetch user: {}", response.text().unwrap());
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not fetch user: {}", data.status_message);
|
||||
|
||||
if data.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidCodeError(0));
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +93,9 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
let path_chunks: Vec<&str> = path.split("/").collect();
|
||||
if path_chunks.len() != 3 {
|
||||
app.emit("auth/failed", ()).unwrap();
|
||||
return Err(RemoteAccessError::InvalidResponse);
|
||||
return Err(RemoteAccessError::HandshakeFailed(
|
||||
"failed to parse token".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let base_url = {
|
||||
|
|
@ -133,6 +137,12 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> {
|
||||
recieve_handshake(app, format!("handshake/{}", token));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
// Tell the app we're processing
|
||||
app.emit("auth/processing", ()).unwrap();
|
||||
|
|
@ -147,7 +157,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
|||
app.emit("auth/finished", ()).unwrap();
|
||||
}
|
||||
|
||||
async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
|
|
@ -159,14 +169,17 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
|||
platform: env::consts::OS.to_string(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send().await?;
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(RemoteAccessError::InvalidRedirect);
|
||||
let data = response.json::<DropServerError>()?;
|
||||
info!("Could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let redir_url = response.text().await?;
|
||||
let redir_url = response.text()?;
|
||||
let complete_redir_url = base_url.join(&redir_url)?;
|
||||
|
||||
info!("opening web browser to continue authentication");
|
||||
|
|
@ -176,8 +189,8 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn auth_initiate<'a>() -> Result<(), String> {
|
||||
let result = auth_initiate_wrapper().await;
|
||||
pub fn auth_initiate<'a>() -> Result<(), String> {
|
||||
let result = auth_initiate_wrapper();
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
}
|
||||
|
|
@ -199,8 +212,10 @@ pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()>
|
|||
|
||||
pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if data.auth.is_some() {
|
||||
if auth.is_some() {
|
||||
let user_result = fetch_user();
|
||||
if user_result.is_err() {
|
||||
let error = user_result.err().unwrap();
|
||||
|
|
@ -215,7 +230,31 @@ pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
|
|||
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
|
||||
}
|
||||
|
||||
drop(data);
|
||||
|
||||
Ok((AppStatus::SignedOut, None))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sign_out(app: AppHandle) -> Result<(), String> {
|
||||
info!("Signing out user");
|
||||
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
{
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedOut;
|
||||
app_state_handle.user = None;
|
||||
}
|
||||
|
||||
// Emit event for frontend
|
||||
app.emit("auth/signedout", ()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
69
src-tauri/src/autostart.rs
Normal file
69
src-tauri/src/autostart.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use crate::DB;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let manager = app.autolaunch();
|
||||
if enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
info!("Enabled autostart");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
info!("Disabled autostart");
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
DB.save().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, String> {
|
||||
// First check DB state
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
// Get actual system state
|
||||
let manager = app.autolaunch();
|
||||
let system_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||
|
||||
// If they don't match, sync to DB state
|
||||
if db_state != system_state {
|
||||
if db_state {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(db_state)
|
||||
}
|
||||
|
||||
// New function to sync state on startup
|
||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
let manager = app.autolaunch();
|
||||
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||
|
||||
if current_state != should_be_enabled {
|
||||
if should_be_enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
info!("Synced autostart: enabled");
|
||||
} else {
|
||||
manager.disable().map_err(|e| e.to_string())?;
|
||||
info!("Synced autostart: disabled");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle) {
|
||||
cleanup_and_exit(&app);
|
||||
}
|
||||
|
||||
|
||||
pub fn cleanup_and_exit(app: &AppHandle, ) {
|
||||
pub fn cleanup_and_exit(app: &AppHandle) {
|
||||
info!("exiting drop application...");
|
||||
|
||||
app.exit(0);
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@ use std::{
|
|||
collections::HashMap,
|
||||
fs::{self, create_dir_all},
|
||||
path::{Path, PathBuf},
|
||||
sync::{LazyLock, Mutex},
|
||||
sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use directories::BaseDirs;
|
||||
use log::debug;
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use tauri::AppHandle;
|
||||
use url::Url;
|
||||
|
||||
use crate::{process::process_manager::Platform, DB};
|
||||
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, games::{library::push_game_update, state::GameStatusManager}, process::process_manager::Platform, DB};
|
||||
|
||||
#[derive(serde::Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DatabaseAuth {
|
||||
pub private: String,
|
||||
pub cert: String,
|
||||
|
|
@ -24,7 +26,7 @@ pub struct DatabaseAuth {
|
|||
// Strings are version names for a particular game
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum GameStatus {
|
||||
pub enum GameDownloadStatus {
|
||||
Remote {},
|
||||
SetupRequired {
|
||||
version_name: String,
|
||||
|
|
@ -38,13 +40,14 @@ pub enum GameStatus {
|
|||
|
||||
// Stuff that shouldn't be synced to disk
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum GameTransientStatus {
|
||||
pub enum ApplicationTransientStatus {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
Updating { version_name: String },
|
||||
Running {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameVersion {
|
||||
pub version_index: usize,
|
||||
|
|
@ -54,24 +57,43 @@ pub struct GameVersion {
|
|||
pub platform: Platform,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DatabaseGames {
|
||||
pub struct DatabaseApplications {
|
||||
pub install_dirs: Vec<String>,
|
||||
// Guaranteed to exist if the game also exists in the app state map
|
||||
pub statuses: HashMap<String, GameStatus>,
|
||||
pub versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub transient_statuses: HashMap<String, GameTransientStatus>,
|
||||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub autostart: bool,
|
||||
// ... other settings ...
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autostart: false,
|
||||
// ... other settings defaults ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Database {
|
||||
#[serde(default)]
|
||||
pub settings: Settings,
|
||||
pub auth: Option<DatabaseAuth>,
|
||||
pub base_url: String,
|
||||
pub games: DatabaseGames,
|
||||
pub applications: DatabaseApplications,
|
||||
pub prev_database: Option<PathBuf>
|
||||
}
|
||||
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
|
||||
|
|
@ -112,21 +134,27 @@ impl DatabaseImpls for DatabaseInterface {
|
|||
debug!("Creating logs directory");
|
||||
create_dir_all(logs_root_dir.clone()).unwrap();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
match exists {
|
||||
true => PathDatabase::load_from_path(db_path).expect("Database loading failed"),
|
||||
true =>
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
|
||||
},
|
||||
false => {
|
||||
let default = Database {
|
||||
settings: Settings::default(),
|
||||
auth: None,
|
||||
base_url: "".to_string(),
|
||||
games: DatabaseGames {
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
|
||||
statuses: HashMap::new(),
|
||||
game_statuses: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
versions: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
},
|
||||
prev_database: None,
|
||||
};
|
||||
debug!(
|
||||
"Creating database at path {}",
|
||||
|
|
@ -172,10 +200,10 @@ pub fn add_download_dir(new_dir: String) -> Result<(), String> {
|
|||
|
||||
// Add it to the dictionary
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
if lock.games.install_dirs.contains(&new_dir) {
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return Err("Download directory already used".to_string());
|
||||
}
|
||||
lock.games.install_dirs.push(new_dir);
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
|
||||
|
|
@ -185,7 +213,7 @@ pub fn add_download_dir(new_dir: String) -> Result<(), String> {
|
|||
#[tauri::command]
|
||||
pub fn delete_download_dir(index: usize) -> Result<(), String> {
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
lock.games.install_dirs.remove(index);
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
|
||||
|
|
@ -197,8 +225,54 @@ pub fn delete_download_dir(index: usize) -> Result<(), String> {
|
|||
#[tauri::command]
|
||||
pub fn fetch_download_dir_stats() -> Result<Vec<String>, String> {
|
||||
let lock = DB.borrow_data().unwrap();
|
||||
let directories = lock.games.install_dirs.clone();
|
||||
let directories = lock.applications.install_dirs.clone();
|
||||
drop(lock);
|
||||
|
||||
Ok(directories)
|
||||
}
|
||||
|
||||
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
|
||||
app_handle: &AppHandle,
|
||||
meta: DownloadableMetadata,
|
||||
setter: F,
|
||||
) {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
setter(&mut db_handle, &meta);
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
push_game_update(app_handle, &meta, status);
|
||||
}
|
||||
|
||||
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||
fn handle_invalid_database(_e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
let mut base = db_path.clone().into_os_string();
|
||||
base.push(".");
|
||||
base.push(time.to_string());
|
||||
base
|
||||
};
|
||||
fs::copy(&db_path, &new_path).unwrap();
|
||||
|
||||
let db = Database {
|
||||
auth: None,
|
||||
base_url: "".to_string(),
|
||||
applications: DatabaseApplications {
|
||||
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
|
||||
game_statuses: HashMap::new(),
|
||||
transient_statuses: HashMap::new(),
|
||||
game_versions: HashMap::new(),
|
||||
installed_game_version: HashMap::new(),
|
||||
},
|
||||
prev_database: Some(new_path.into()),
|
||||
settings: Settings { autostart: false },
|
||||
};
|
||||
|
||||
PathDatabase::create_at_path(db_path, db)
|
||||
.expect("Database could not be created")
|
||||
|
||||
|
||||
}
|
||||
23
src-tauri/src/debug.rs
Normal file
23
src-tauri/src/debug.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::{DATA_ROOT_DIR, DB};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemData {
|
||||
client_id: String,
|
||||
base_url: String,
|
||||
data_dir: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_system_data() -> Result<SystemData, String> {
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let system_data = SystemData {
|
||||
client_id: db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
base_url: db_handle.base_url.clone(),
|
||||
data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
|
||||
};
|
||||
drop(db_handle);
|
||||
|
||||
Ok(system_data)
|
||||
}
|
||||
41
src-tauri/src/download_manager/application_download_error.rs
Normal file
41
src-tauri/src/download_manager/application_download_error.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::{fmt::{Display, Formatter}, io};
|
||||
|
||||
use crate::remote::RemoteAccessError;
|
||||
|
||||
// TODO: Rename / separate from downloads
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ApplicationDownloadError {
|
||||
Communication(RemoteAccessError),
|
||||
Checksum,
|
||||
Setup(SetupError),
|
||||
Lock,
|
||||
IoError(io::ErrorKind),
|
||||
DownloadError,
|
||||
}
|
||||
|
||||
impl Display for ApplicationDownloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
|
||||
ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
|
||||
ApplicationDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
|
||||
ApplicationDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
|
||||
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
|
||||
ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SetupError {
|
||||
Context,
|
||||
}
|
||||
|
||||
impl Display for SetupError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SetupError::Context => write!(f, "Failed to generate contexts for download"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -12,47 +12,57 @@ use std::{
|
|||
use log::info;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{
|
||||
download_agent::{GameDownloadAgent, GameDownloadError},
|
||||
download_manager_builder::CurrentProgressObject,
|
||||
progress_object::ProgressObject,
|
||||
queue::Queue,
|
||||
};
|
||||
|
||||
use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue};
|
||||
|
||||
pub enum DownloadManagerSignal {
|
||||
/// Resumes (or starts) the DownloadManager
|
||||
Go,
|
||||
/// Pauses the DownloadManager
|
||||
Stop,
|
||||
/// Called when a GameDownloadAgent has fully completed a download.
|
||||
Completed(String),
|
||||
/// Generates and appends a GameDownloadAgent
|
||||
/// Called when a DownloadAgent has fully completed a download.
|
||||
Completed(DownloadableMetadata),
|
||||
/// Generates and appends a DownloadAgent
|
||||
/// to the registry and queue
|
||||
Queue(String, String, usize),
|
||||
Queue(DownloadAgent),
|
||||
/// Tells the Manager to stop the current
|
||||
/// download, sync everything to disk, and
|
||||
/// then exit
|
||||
Finish,
|
||||
/// Stops (but doesn't remove) current download
|
||||
Cancel,
|
||||
/// Removes a given game
|
||||
Remove(String),
|
||||
/// Stops, removes, and tells a download to cleanup
|
||||
Cancel(DownloadableMetadata),
|
||||
/// Removes a given application
|
||||
Remove(DownloadableMetadata),
|
||||
/// Any error which occurs in the agent
|
||||
Error(GameDownloadError),
|
||||
Error(ApplicationDownloadError),
|
||||
/// Pushes UI update
|
||||
Update,
|
||||
UpdateUIQueue,
|
||||
UpdateUIStats(usize, usize), //kb/s and seconds
|
||||
/// Uninstall download
|
||||
/// Takes download ID
|
||||
Uninstall(DownloadableMetadata),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DownloadManagerStatus {
|
||||
Downloading,
|
||||
Paused,
|
||||
Empty,
|
||||
Error(GameDownloadError),
|
||||
Error(ApplicationDownloadError),
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub enum GameDownloadStatus {
|
||||
impl Serialize for DownloadManagerStatus {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!["{:?}", self])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub enum DownloadStatus {
|
||||
Queued,
|
||||
Downloading,
|
||||
Error,
|
||||
|
|
@ -74,27 +84,6 @@ pub struct DownloadManager {
|
|||
progress: CurrentProgressObject,
|
||||
command_sender: Sender<DownloadManagerSignal>,
|
||||
}
|
||||
pub struct GameDownloadAgentQueueStandin {
|
||||
pub id: String,
|
||||
pub status: Mutex<GameDownloadStatus>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
}
|
||||
impl From<Arc<GameDownloadAgent>> for GameDownloadAgentQueueStandin {
|
||||
fn from(value: Arc<GameDownloadAgent>) -> Self {
|
||||
Self {
|
||||
id: value.id.clone(),
|
||||
status: Mutex::from(GameDownloadStatus::Queued),
|
||||
progress: value.progress.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Debug for GameDownloadAgentQueueStandin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GameDownloadAgentQueueStandin")
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DownloadManager {
|
||||
|
|
@ -112,42 +101,36 @@ impl DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn queue_game(
|
||||
pub fn queue_download(
|
||||
&self,
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
download: DownloadAgent
|
||||
) -> Result<(), SendError<DownloadManagerSignal>> {
|
||||
info!("Adding game id {}", id);
|
||||
self.command_sender.send(DownloadManagerSignal::Queue(
|
||||
id,
|
||||
version,
|
||||
target_download_dir,
|
||||
))?;
|
||||
info!("Adding download id {:?}", download.metadata());
|
||||
self.command_sender.send(DownloadManagerSignal::Queue(download))?;
|
||||
self.command_sender.send(DownloadManagerSignal::Go)
|
||||
}
|
||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<Arc<GameDownloadAgentQueueStandin>>> {
|
||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
|
||||
self.download_queue.edit()
|
||||
}
|
||||
pub fn read_queue(&self) -> VecDeque<Arc<GameDownloadAgentQueueStandin>> {
|
||||
pub fn read_queue(&self) -> VecDeque<DownloadableMetadata> {
|
||||
self.download_queue.read()
|
||||
}
|
||||
pub fn get_current_game_download_progress(&self) -> Option<f64> {
|
||||
pub fn get_current_download_progress(&self) -> Option<f64> {
|
||||
let progress_object = (*self.progress.lock().unwrap()).clone()?;
|
||||
Some(progress_object.get_progress())
|
||||
}
|
||||
pub fn rearrange_string(&self, id: String, new_index: usize) {
|
||||
pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
|
||||
let mut queue = self.edit();
|
||||
let current_index = get_index_from_id(&mut queue, id).unwrap();
|
||||
let current_index = get_index_from_id(&mut queue, meta).unwrap();
|
||||
let to_move = queue.remove(current_index).unwrap();
|
||||
queue.insert(new_index, to_move);
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Update)
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
pub fn cancel(&self, game_id: String) {
|
||||
pub fn cancel(&self, meta: DownloadableMetadata) {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Remove(game_id))
|
||||
.send(DownloadManagerSignal::Cancel(meta))
|
||||
.unwrap();
|
||||
}
|
||||
pub fn rearrange(&self, current_index: usize, new_index: usize) {
|
||||
|
|
@ -158,7 +141,7 @@ impl DownloadManager {
|
|||
let needs_pause = current_index == 0 || new_index == 0;
|
||||
if needs_pause {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Cancel)
|
||||
.send(DownloadManagerSignal::Stop)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +157,7 @@ impl DownloadManager {
|
|||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Update)
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
pub fn pause_downloads(&self) {
|
||||
|
|
@ -191,15 +174,23 @@ impl DownloadManager {
|
|||
.unwrap();
|
||||
self.terminator.join()
|
||||
}
|
||||
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Uninstall(meta))
|
||||
.unwrap();
|
||||
}
|
||||
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
||||
self.command_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes in the locked value from .edit() and attempts to
|
||||
/// get the index of whatever game_id is passed in
|
||||
/// get the index of whatever id is passed in
|
||||
fn get_index_from_id(
|
||||
queue: &mut MutexGuard<'_, VecDeque<Arc<GameDownloadAgentQueueStandin>>>,
|
||||
id: String,
|
||||
queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>,
|
||||
meta: &DownloadableMetadata,
|
||||
) -> Option<usize> {
|
||||
queue
|
||||
.iter()
|
||||
.position(|download_agent| download_agent.id == id)
|
||||
.position(|download_agent| download_agent == meta)
|
||||
}
|
||||
343
src-tauri/src/download_manager/download_manager_builder.rs
Normal file
343
src-tauri/src/download_manager/download_manager_builder.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::remove_dir_all,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex, RwLockWriteGuard,
|
||||
},
|
||||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent};
|
||||
|
||||
use super::{application_download_error::ApplicationDownloadError, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, queue::Queue};
|
||||
|
||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
||||
|
||||
/*
|
||||
|
||||
Welcome to the download manager, the most overengineered, glorious piece of bullshit.
|
||||
|
||||
The download manager takes a queue of ids and their associated
|
||||
DownloadAgents, and then, one-by-one, executes them. It provides an interface
|
||||
to interact with the currently downloading agent, and manage the queue.
|
||||
|
||||
When the DownloadManager is initialised, it is designed to provide a reference
|
||||
which can be used to provide some instructions (the DownloadManagerInterface),
|
||||
but other than that, it runs without any sort of interruptions.
|
||||
|
||||
It does this by opening up two data structures. Primarily is the command_receiver,
|
||||
and mpsc (multi-channel-single-producer) which allows commands to be sent from
|
||||
the Interface, and queued up for the Manager to process.
|
||||
|
||||
These have been mapped in the DownloadManagerSignal docs.
|
||||
|
||||
The other way to interact with the DownloadManager is via the donwload_queue,
|
||||
which is just a collection of ids which may be rearranged to suit
|
||||
whichever download queue order is required.
|
||||
|
||||
+----------------------------------------------------------------------------+
|
||||
| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
|
||||
| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
|
||||
| WHICH HAS NOT BEEN ACCOUNTED FOR |
|
||||
+----------------------------------------------------------------------------+
|
||||
|
||||
This download queue does not actually own any of the DownloadAgents. It is
|
||||
simply a id-based reference system. The actual Agents are stored in the
|
||||
download_agent_registry HashMap, as ordering is no issue here. This is why
|
||||
appending or removing from the download_queue must be done via signals.
|
||||
|
||||
Behold, my madness - quexeky
|
||||
|
||||
*/
|
||||
|
||||
pub struct DownloadManagerBuilder {
|
||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||
download_queue: Queue,
|
||||
command_receiver: Receiver<DownloadManagerSignal>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
progress: CurrentProgressObject,
|
||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||
app_handle: AppHandle,
|
||||
|
||||
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
||||
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||
active_control_flag: Option<DownloadThreadControl>,
|
||||
}
|
||||
impl DownloadManagerBuilder {
|
||||
pub fn build(app_handle: AppHandle) -> DownloadManager {
|
||||
let queue = Queue::new();
|
||||
let (command_sender, command_receiver) = channel();
|
||||
let active_progress = Arc::new(Mutex::new(None));
|
||||
let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
|
||||
|
||||
let manager = Self {
|
||||
download_agent_registry: HashMap::new(),
|
||||
download_queue: queue.clone(),
|
||||
command_receiver,
|
||||
status: status.clone(),
|
||||
sender: command_sender.clone(),
|
||||
progress: active_progress.clone(),
|
||||
app_handle,
|
||||
|
||||
current_download_agent: None,
|
||||
current_download_thread: Mutex::new(None),
|
||||
active_control_flag: None,
|
||||
};
|
||||
|
||||
let terminator = spawn(|| manager.manage_queue());
|
||||
|
||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||
}
|
||||
|
||||
fn set_status(&self, status: DownloadManagerStatus) {
|
||||
*self.status.lock().unwrap() = status;
|
||||
}
|
||||
|
||||
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||
self.download_queue.pop_front();
|
||||
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
||||
self.cleanup_current_download();
|
||||
download_agent
|
||||
}
|
||||
|
||||
// CAREFUL WITH THIS FUNCTION
|
||||
// Make sure the download thread is terminated
|
||||
fn cleanup_current_download(&mut self) {
|
||||
self.active_control_flag = None;
|
||||
*self.progress.lock().unwrap() = None;
|
||||
self.current_download_agent = None;
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
*download_thread_lock = None;
|
||||
drop(download_thread_lock);
|
||||
|
||||
}
|
||||
|
||||
fn stop_and_wait_current_download(&self) {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(current_flag) = &self.active_control_flag {
|
||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||
current_download_thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
loop {
|
||||
let signal = match self.command_receiver.recv() {
|
||||
Ok(signal) => signal,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
match signal {
|
||||
DownloadManagerSignal::Go => {
|
||||
self.manage_go_signal();
|
||||
}
|
||||
DownloadManagerSignal::Stop => {
|
||||
self.manage_stop_signal();
|
||||
}
|
||||
DownloadManagerSignal::Completed(meta) => {
|
||||
self.manage_completed_signal(meta);
|
||||
}
|
||||
DownloadManagerSignal::Queue(download_agent) => {
|
||||
self.manage_queue_signal(download_agent);
|
||||
}
|
||||
DownloadManagerSignal::Error(e) => {
|
||||
self.manage_error_signal(e);
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIQueue => {
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||
self.push_ui_stats_update(kbs, time);
|
||||
}
|
||||
DownloadManagerSignal::Finish => {
|
||||
self.stop_and_wait_current_download();
|
||||
return Ok(());
|
||||
}
|
||||
DownloadManagerSignal::Cancel(meta) => {
|
||||
self.manage_cancel_signal(&meta);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
info!("Got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
info!("Meta: {:?}", meta);
|
||||
|
||||
if self.download_queue.exists(meta.clone()) {
|
||||
info!("Download with same ID already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
download_agent.on_initialised(&self.app_handle);
|
||||
self.download_queue.append(meta.clone());
|
||||
self.download_agent_registry.insert(meta, download_agent);
|
||||
|
||||
self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
info!("Got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
info!("Download agent registry: {:?}", self.download_agent_registry.len());
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current_download_agent.is_some() {
|
||||
info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata());
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Current download queue: {:?}", self.download_queue.read());
|
||||
|
||||
// Should always be Some if the above two statements keep going
|
||||
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||
|
||||
info!("Starting download for {:?}", agent_data);
|
||||
|
||||
let download_agent = self
|
||||
.download_agent_registry
|
||||
.get(&agent_data)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
self.active_control_flag = Some(download_agent.control_flag());
|
||||
self.current_download_agent = Some(download_agent.clone());
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
let app_handle = self.app_handle.clone();
|
||||
|
||||
*download_thread_lock = Some(spawn(move || {
|
||||
match download_agent.download(&app_handle) {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(true) => {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap();
|
||||
},
|
||||
// Ok(false) is for incomplete but exited properly
|
||||
Ok(false) => {
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
},
|
||||
Err(e) => {
|
||||
download_agent.on_error(&app_handle, e.clone());
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
},
|
||||
}
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}));
|
||||
|
||||
self.set_status(DownloadManagerStatus::Downloading);
|
||||
let active_control_flag = self.active_control_flag.clone().unwrap();
|
||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
}
|
||||
fn manage_stop_signal(&mut self) {
|
||||
info!("Got signal Stop");
|
||||
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
}
|
||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
info!("Got signal Completed");
|
||||
if let Some(interface) = &self.current_download_agent {
|
||||
if interface.metadata() == meta {
|
||||
info!("Popping consumed data");
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
}
|
||||
self.push_ui_queue_update();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
info!("Got signal Error");
|
||||
let current_agent = self.current_download_agent.clone().unwrap();
|
||||
|
||||
current_agent.on_error(&self.app_handle, error.clone());
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
info!("Got signal Cancel");
|
||||
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if ¤t_download.metadata() == meta {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
current_download.on_cancelled(&self.app_handle);
|
||||
self.stop_and_wait_current_download();
|
||||
|
||||
self.download_queue.pop_front();
|
||||
|
||||
self.cleanup_current_download();
|
||||
info!("Current donwload queue: {:?}", self.download_queue.read());
|
||||
}
|
||||
// TODO: Collapse these two into a single if statement somehow
|
||||
else {
|
||||
if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
info!("Object exists in registry");
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
info!("Object exists in registry");
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||
|
||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||
}
|
||||
fn push_ui_queue_update(&self) {
|
||||
let queue = &self.download_queue.read();
|
||||
let queue_objs = queue
|
||||
.iter()
|
||||
.map(|(key)| {
|
||||
let val = self.download_agent_registry.get(key).unwrap();
|
||||
QueueUpdateEventQueueData {
|
||||
meta: DownloadableMetadata::clone(&key),
|
||||
status: val.status(),
|
||||
progress: val.progress().get_progress()
|
||||
}})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent {
|
||||
queue: queue_objs,
|
||||
};
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
}
|
||||
}
|
||||
20
src-tauri/src/download_manager/downloadable.rs
Normal file
20
src-tauri/src/download_manager/downloadable.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}};
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use super::{
|
||||
application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject
|
||||
};
|
||||
|
||||
pub trait Downloadable: Send + Sync {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
fn progress(&self) -> Arc<ProgressObject>;
|
||||
fn control_flag(&self) -> DownloadThreadControl;
|
||||
fn status(&self) -> DownloadStatus;
|
||||
fn metadata(&self) -> DownloadableMetadata;
|
||||
fn on_initialised(&self, app_handle: &AppHandle);
|
||||
fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError);
|
||||
fn on_complete(&self, app_handle: &AppHandle);
|
||||
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||
}
|
||||
26
src-tauri/src/download_manager/downloadable_metadata.rs
Normal file
26
src-tauri/src/download_manager/downloadable_metadata.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
|
||||
pub enum DownloadType {
|
||||
Game,
|
||||
Tool,
|
||||
DLC,
|
||||
Mod
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadableMetadata {
|
||||
pub id: String,
|
||||
pub version: Option<String>,
|
||||
pub download_type: DownloadType
|
||||
}
|
||||
impl DownloadableMetadata {
|
||||
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
download_type
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src-tauri/src/download_manager/generate_downloadable.rs
Normal file
7
src-tauri/src/download_manager/generate_downloadable.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::{download_manager_builder::DownloadAgent, downloadable_metadata::DownloadableMetadata};
|
||||
|
||||
pub fn generate_downloadable(meta: DownloadableMetadata) -> DownloadAgent {
|
||||
todo!()
|
||||
}
|
||||
9
src-tauri/src/download_manager/mod.rs
Normal file
9
src-tauri/src/download_manager/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod download_manager;
|
||||
pub mod download_manager_builder;
|
||||
pub mod progress_object;
|
||||
pub mod queue;
|
||||
pub mod download_thread_control_flag;
|
||||
pub mod downloadable;
|
||||
pub mod application_download_error;
|
||||
pub mod downloadable_metadata;
|
||||
pub mod generate_downloadable;
|
||||
|
|
@ -2,7 +2,7 @@ use std::{
|
|||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::Sender,
|
||||
Arc, Mutex,
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
|
|
@ -19,7 +19,9 @@ pub struct ProgressObject {
|
|||
sender: Sender<DownloadManagerSignal>,
|
||||
|
||||
points_towards_update: Arc<AtomicUsize>,
|
||||
points_to_push_update: Arc<Mutex<usize>>,
|
||||
points_to_push_update: Arc<AtomicUsize>,
|
||||
last_update: Arc<RwLock<Instant>>,
|
||||
amount_last_update: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
pub struct ProgressHandle {
|
||||
|
|
@ -58,7 +60,9 @@ impl ProgressObject {
|
|||
sender,
|
||||
|
||||
points_towards_update: Arc::new(AtomicUsize::new(0)),
|
||||
points_to_push_update: Arc::new(Mutex::new(points_to_push_update)),
|
||||
points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
|
||||
last_update: Arc::new(RwLock::new(Instant::now())),
|
||||
amount_last_update: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,16 +71,44 @@ impl ProgressObject {
|
|||
.points_towards_update
|
||||
.fetch_add(amount_added, Ordering::Relaxed);
|
||||
|
||||
let to_update_handle = self.points_to_push_update.lock().unwrap();
|
||||
let to_update = *to_update_handle;
|
||||
drop(to_update_handle);
|
||||
let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
|
||||
|
||||
if current_amount < to_update {
|
||||
return;
|
||||
if current_amount >= to_update {
|
||||
self.points_towards_update
|
||||
.fetch_sub(to_update, Ordering::Relaxed);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let last_update = self.last_update.read().unwrap();
|
||||
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
|
||||
if last_update_difference > 1000 {
|
||||
// push update
|
||||
drop(last_update);
|
||||
let mut last_update = self.last_update.write().unwrap();
|
||||
*last_update = Instant::now();
|
||||
drop(last_update);
|
||||
|
||||
let current_amount = self.sum();
|
||||
let max = self.get_max();
|
||||
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
|
||||
self.amount_last_update
|
||||
.store(current_amount, Ordering::Relaxed);
|
||||
|
||||
let amount_since_last_update = current_amount - amount_at_last_update;
|
||||
|
||||
let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1);
|
||||
|
||||
let remaining = max - current_amount; // bytes
|
||||
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::UpdateUIStats(
|
||||
kilobytes_per_second,
|
||||
time_remaining,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
self.points_towards_update
|
||||
.fetch_sub(to_update, Ordering::Relaxed);
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
}
|
||||
|
||||
pub fn set_time_now(&self) {
|
||||
|
|
@ -95,7 +127,8 @@ impl ProgressObject {
|
|||
}
|
||||
pub fn set_max(&self, new_max: usize) {
|
||||
*self.max.lock().unwrap() = new_max;
|
||||
*self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES;
|
||||
self.points_to_push_update
|
||||
.store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
|
||||
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
|
||||
}
|
||||
pub fn set_size(&self, length: usize) {
|
||||
|
|
@ -3,11 +3,11 @@ use std::{
|
|||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use super::download_manager::GameDownloadAgentQueueStandin;
|
||||
use super::downloadable_metadata::DownloadableMetadata;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Queue {
|
||||
inner: Arc<Mutex<VecDeque<Arc<GameDownloadAgentQueueStandin>>>>,
|
||||
inner: Arc<Mutex<VecDeque<DownloadableMetadata>>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -17,49 +17,52 @@ impl Queue {
|
|||
inner: Arc::new(Mutex::new(VecDeque::new())),
|
||||
}
|
||||
}
|
||||
pub fn read(&self) -> VecDeque<Arc<GameDownloadAgentQueueStandin>> {
|
||||
pub fn read(&self) -> VecDeque<DownloadableMetadata> {
|
||||
self.inner.lock().unwrap().clone()
|
||||
}
|
||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<Arc<GameDownloadAgentQueueStandin>>> {
|
||||
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
|
||||
self.inner.lock().unwrap()
|
||||
}
|
||||
pub fn pop_front(&self) -> Option<Arc<GameDownloadAgentQueueStandin>> {
|
||||
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
||||
self.edit().pop_front()
|
||||
}
|
||||
pub fn empty(&self) -> bool {
|
||||
self.inner.lock().unwrap().len() == 0
|
||||
}
|
||||
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
|
||||
self.read().contains(&meta)
|
||||
}
|
||||
/// Either inserts `interface` at the specified index, or appends to
|
||||
/// the back of the deque if index is greater than the length of the deque
|
||||
pub fn insert(&self, interface: GameDownloadAgentQueueStandin, index: usize) {
|
||||
pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
|
||||
if self.read().len() > index {
|
||||
self.append(interface);
|
||||
} else {
|
||||
self.edit().insert(index, Arc::new(interface));
|
||||
self.edit().insert(index, interface);
|
||||
}
|
||||
}
|
||||
pub fn append(&self, interface: GameDownloadAgentQueueStandin) {
|
||||
self.edit().push_back(Arc::new(interface));
|
||||
pub fn append(&self, interface: DownloadableMetadata) {
|
||||
self.edit().push_back(interface);
|
||||
}
|
||||
pub fn pop_front_if_equal(
|
||||
&self,
|
||||
game_id: String,
|
||||
) -> Option<Arc<GameDownloadAgentQueueStandin>> {
|
||||
meta: &DownloadableMetadata,
|
||||
) -> Option<DownloadableMetadata> {
|
||||
let mut queue = self.edit();
|
||||
let front = match queue.front() {
|
||||
Some(front) => front,
|
||||
None => return None,
|
||||
};
|
||||
if front.id == game_id {
|
||||
if front == meta {
|
||||
return queue.pop_front();
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn get_by_id(&self, game_id: String) -> Option<usize> {
|
||||
self.read().iter().position(|data| data.id == game_id)
|
||||
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
|
||||
self.read().iter().position(|data| data == meta)
|
||||
}
|
||||
pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> {
|
||||
let index = match self.get_by_id(game_id) {
|
||||
pub fn move_to_index_by_meta(&self, meta: &DownloadableMetadata, new_index: usize) -> Result<(), ()> {
|
||||
let index = match self.get_by_meta(meta) {
|
||||
Some(index) => index,
|
||||
None => return Err(()),
|
||||
};
|
||||
|
|
@ -1,335 +0,0 @@
|
|||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::downloads::progress_object::ProgressHandle;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use core::time;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use serde::ser::{Error, SerializeMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::download_manager::DownloadManagerSignal;
|
||||
use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use super::progress_object::ProgressObject;
|
||||
use super::stored_manifest::StoredManifest;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Vec<DropDownloadContext>,
|
||||
completed_contexts: Mutex<Vec<usize>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GameDownloadError {
|
||||
Communication(RemoteAccessError),
|
||||
Checksum,
|
||||
Setup(SetupError),
|
||||
Lock,
|
||||
IoError(io::Error),
|
||||
DownloadError,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SetupError {
|
||||
Context,
|
||||
}
|
||||
|
||||
impl Display for GameDownloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GameDownloadError::Communication(error) => write!(f, "{}", error),
|
||||
GameDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
|
||||
GameDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
|
||||
GameDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
|
||||
GameDownloadError::IoError(error) => write!(f, "{}", error),
|
||||
GameDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SetupError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SetupError::Context => write!(f, "Failed to generate contexts for download"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub fn new(
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let base_dir = db_lock.games.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
let base_dir_path = Path::new(&base_dir);
|
||||
let data_base_dir_path = base_dir_path.join(id.clone());
|
||||
|
||||
let stored_manifest =
|
||||
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Vec::new(),
|
||||
completed_contexts: Mutex::new(Vec::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&mut self) -> Result<(), GameDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
info!("Ensured manifest exists");
|
||||
|
||||
self.ensure_contexts()?;
|
||||
info!("Ensured contexts exists");
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn download(&mut self) -> Result<(), GameDownloadError> {
|
||||
self.setup_download()?;
|
||||
self.set_progress_object_params();
|
||||
let timer = Instant::now();
|
||||
self.run().map_err(|_| GameDownloadError::DownloadError)?;
|
||||
|
||||
info!(
|
||||
"{} took {}ms to download",
|
||||
self.id,
|
||||
timer.elapsed().as_millis()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
|
||||
if self.manifest.lock().unwrap().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_manifest()
|
||||
}
|
||||
|
||||
fn download_manifest(&self) -> Result<(), GameDownloadError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
let manifest_url = base_url
|
||||
.join(
|
||||
format!(
|
||||
"/api/v1/client/metadata/manifest?id={}&version={}",
|
||||
self.id,
|
||||
encode(&self.version)
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(GameDownloadError::Communication(
|
||||
RemoteAccessError::ManifestDownloadFailed(
|
||||
response.status(),
|
||||
response.text().unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(GameDownloadError::Lock)
|
||||
}
|
||||
|
||||
fn set_progress_object_params(&self) {
|
||||
// Avoid re-setting it
|
||||
if self.progress.get_max() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let length = self.contexts.len();
|
||||
|
||||
let chunk_count = self.contexts.iter().map(|chunk| chunk.length).sum();
|
||||
|
||||
debug!("Setting ProgressObject max to {}", chunk_count);
|
||||
self.progress.set_max(chunk_count);
|
||||
debug!("Setting ProgressObject size to {}", length);
|
||||
self.progress.set_size(length);
|
||||
debug!("Setting ProgressObject time to now");
|
||||
self.progress.set_time_now();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&mut self) -> Result<(), GameDownloadError> {
|
||||
if !self.contexts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.generate_contexts()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_contexts(&mut self) -> Result<(), GameDownloadError> {
|
||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
*self.completed_contexts.lock().unwrap() = self.stored_manifest.get_completed_contexts();
|
||||
|
||||
info!(
|
||||
"Completed contexts: {:?}",
|
||||
*self.completed_contexts.lock().unwrap()
|
||||
);
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
let path = base_path.join(Path::new(&raw_path));
|
||||
|
||||
let container = path.parent().unwrap();
|
||||
create_dir_all(container).unwrap();
|
||||
|
||||
let file = File::create(path.clone()).unwrap();
|
||||
let mut running_offset = 0;
|
||||
|
||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||
contexts.push(DropDownloadContext {
|
||||
file_name: raw_path.to_string(),
|
||||
version: chunk.version_name.to_string(),
|
||||
offset: running_offset,
|
||||
index,
|
||||
game_id: game_id.to_string(),
|
||||
path: path.clone(),
|
||||
checksum: chunk.checksums[index].clone(),
|
||||
length: *length,
|
||||
permissions: chunk.permissions,
|
||||
});
|
||||
running_offset += *length as u64;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
self.contexts = contexts;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), ()> {
|
||||
info!("downloading game: {}", self.id);
|
||||
const DOWNLOAD_MAX_THREADS: usize = 1;
|
||||
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(DOWNLOAD_MAX_THREADS)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let completed_indexes = Arc::new(Mutex::new(Vec::new()));
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
|
||||
pool.scope(move |scope| {
|
||||
let completed_lock = self.completed_contexts.lock().unwrap();
|
||||
|
||||
for (index, context) in self.contexts.iter().enumerate() {
|
||||
let progress = self.progress.get(index); // Clone arcs
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
// If we've done this one already, skip it
|
||||
if completed_lock.contains(&index) {
|
||||
progress_handle.add(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let context = context.clone();
|
||||
let control_flag = self.control_flag.clone(); // Clone arcs
|
||||
let completed_indexes_ref = completed_indexes_loop_arc.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context.clone(), control_flag, progress_handle) {
|
||||
Ok(res) => {
|
||||
if res {
|
||||
let mut lock = completed_indexes_ref.lock().unwrap();
|
||||
lock.push(index);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("GameDownloadError: {}", e);
|
||||
self.sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let completed_lock_len = {
|
||||
let mut completed_lock = self.completed_contexts.lock().unwrap();
|
||||
let newly_completed_lock = completed_indexes.lock().unwrap();
|
||||
|
||||
completed_lock.extend(newly_completed_lock.iter());
|
||||
|
||||
completed_lock.len()
|
||||
};
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != self.contexts.len() {
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(&self.completed_contexts);
|
||||
info!("Setting completed contexts");
|
||||
self.stored_manifest.write();
|
||||
info!("Wrote completed contexts");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We've completed
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Completed(self.id.clone()))
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex, RwLockWriteGuard,
|
||||
},
|
||||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::{
|
||||
db::{Database, GameStatus, GameTransientStatus},
|
||||
library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData},
|
||||
state::GameStatusManager,
|
||||
DB,
|
||||
};
|
||||
|
||||
use super::{
|
||||
download_agent::{GameDownloadAgent, GameDownloadError},
|
||||
download_manager::{
|
||||
DownloadManager, DownloadManagerSignal, DownloadManagerStatus,
|
||||
GameDownloadAgentQueueStandin, GameDownloadStatus,
|
||||
},
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::ProgressObject,
|
||||
queue::Queue,
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Welcome to the download manager, the most overengineered, glorious piece of bullshit.
|
||||
|
||||
The download manager takes a queue of game_ids and their associated
|
||||
GameDownloadAgents, and then, one-by-one, executes them. It provides an interface
|
||||
to interact with the currently downloading agent, and manage the queue.
|
||||
|
||||
When the DownloadManager is initialised, it is designed to provide a reference
|
||||
which can be used to provide some instructions (the DownloadManagerInterface),
|
||||
but other than that, it runs without any sort of interruptions.
|
||||
|
||||
It does this by opening up two data structures. Primarily is the command_receiver,
|
||||
and mpsc (multi-channel-single-producer) which allows commands to be sent from
|
||||
the Interface, and queued up for the Manager to process.
|
||||
|
||||
These have been mapped in the DownloadManagerSignal docs.
|
||||
|
||||
The other way to interact with the DownloadManager is via the donwload_queue,
|
||||
which is just a collection of ids which may be rearranged to suit
|
||||
whichever download queue order is required.
|
||||
|
||||
+----------------------------------------------------------------------------+
|
||||
| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
|
||||
| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
|
||||
| WHICH HAS NOT BEEN ACCOUNTED FOR |
|
||||
+----------------------------------------------------------------------------+
|
||||
|
||||
This download queue does not actually own any of the GameDownloadAgents. It is
|
||||
simply a id-based reference system. The actual Agents are stored in the
|
||||
download_agent_registry HashMap, as ordering is no issue here. This is why
|
||||
appending or removing from the download_queue must be done via signals.
|
||||
|
||||
Behold, my madness - quexeky
|
||||
|
||||
*/
|
||||
|
||||
// Refactored to consolidate this type. It's a monster.
|
||||
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
||||
|
||||
pub struct DownloadManagerBuilder {
|
||||
download_agent_registry: HashMap<String, Arc<Mutex<GameDownloadAgent>>>,
|
||||
download_queue: Queue,
|
||||
command_receiver: Receiver<DownloadManagerSignal>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
progress: CurrentProgressObject,
|
||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||
app_handle: AppHandle,
|
||||
|
||||
current_download_agent: Option<Arc<GameDownloadAgentQueueStandin>>, // Should be the only game download agent in the map with the "Go" flag
|
||||
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||
active_control_flag: Option<DownloadThreadControl>,
|
||||
}
|
||||
|
||||
impl DownloadManagerBuilder {
|
||||
pub fn build(app_handle: AppHandle) -> DownloadManager {
|
||||
let queue = Queue::new();
|
||||
let (command_sender, command_receiver) = channel();
|
||||
let active_progress = Arc::new(Mutex::new(None));
|
||||
let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
|
||||
|
||||
let manager = Self {
|
||||
download_agent_registry: HashMap::new(),
|
||||
download_queue: queue.clone(),
|
||||
command_receiver,
|
||||
status: status.clone(),
|
||||
sender: command_sender.clone(),
|
||||
progress: active_progress.clone(),
|
||||
app_handle,
|
||||
|
||||
current_download_agent: None,
|
||||
current_download_thread: Mutex::new(None),
|
||||
active_control_flag: None,
|
||||
};
|
||||
|
||||
let terminator = spawn(|| manager.manage_queue());
|
||||
|
||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||
}
|
||||
|
||||
fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &String) -> ()>(
|
||||
&self,
|
||||
id: String,
|
||||
setter: F,
|
||||
) {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
setter(&mut db_handle, &id);
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
|
||||
self.app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", id),
|
||||
GameUpdateEvent {
|
||||
game_id: id,
|
||||
status,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn push_manager_update(&self) {
|
||||
let queue = self.download_queue.read();
|
||||
let queue_objs: Vec<QueueUpdateEventQueueData> = queue
|
||||
.iter()
|
||||
.map(|interface| QueueUpdateEventQueueData {
|
||||
id: interface.id.clone(),
|
||||
status: interface.status.lock().unwrap().clone(),
|
||||
progress: interface.progress.get_progress(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
}
|
||||
|
||||
fn stop_and_wait_current_download(&self) {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(current_flag) = &self.active_control_flag {
|
||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||
current_download_thread.join().unwrap();
|
||||
}
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
fn sync_download_agent(&self) {}
|
||||
|
||||
fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc<Mutex<GameDownloadAgent>> {
|
||||
self.download_queue.pop_front();
|
||||
let download_agent = self.download_agent_registry.remove(game_id).unwrap();
|
||||
self.cleanup_current_download();
|
||||
download_agent
|
||||
}
|
||||
|
||||
// CAREFUL WITH THIS FUNCTION
|
||||
// Make sure the download thread is terminated
|
||||
fn cleanup_current_download(&mut self) {
|
||||
self.active_control_flag = None;
|
||||
*self.progress.lock().unwrap() = None;
|
||||
self.current_download_agent = None;
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
*download_thread_lock = None;
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
loop {
|
||||
let signal = match self.command_receiver.recv() {
|
||||
Ok(signal) => signal,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
match signal {
|
||||
DownloadManagerSignal::Go => {
|
||||
self.manage_go_signal();
|
||||
}
|
||||
DownloadManagerSignal::Stop => {
|
||||
self.manage_stop_signal();
|
||||
}
|
||||
DownloadManagerSignal::Completed(game_id) => {
|
||||
self.manage_completed_signal(game_id);
|
||||
}
|
||||
DownloadManagerSignal::Queue(game_id, version, target_download_dir) => {
|
||||
self.manage_queue_signal(game_id, version, target_download_dir);
|
||||
}
|
||||
DownloadManagerSignal::Error(e) => {
|
||||
self.manage_error_signal(e);
|
||||
}
|
||||
DownloadManagerSignal::Cancel => {
|
||||
self.manage_cancel_signal();
|
||||
}
|
||||
DownloadManagerSignal::Update => {
|
||||
self.push_manager_update();
|
||||
}
|
||||
DownloadManagerSignal::Finish => {
|
||||
self.stop_and_wait_current_download();
|
||||
return Ok(());
|
||||
}
|
||||
DownloadManagerSignal::Remove(game_id) => {
|
||||
self.manage_remove_game(game_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_remove_game(&mut self, game_id: String) {
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if current_download.id == game_id {
|
||||
self.manage_cancel_signal();
|
||||
}
|
||||
}
|
||||
|
||||
let index = self.download_queue.get_by_id(game_id.clone()).unwrap();
|
||||
let mut queue_handle = self.download_queue.edit();
|
||||
queue_handle.remove(index);
|
||||
self.set_game_status(game_id, |db_handle, id| {
|
||||
db_handle.games.transient_statuses.remove(id);
|
||||
});
|
||||
drop(queue_handle);
|
||||
|
||||
if self.current_download_agent.is_none() {
|
||||
self.manage_go_signal();
|
||||
}
|
||||
|
||||
self.push_manager_update();
|
||||
}
|
||||
|
||||
fn manage_stop_signal(&mut self) {
|
||||
info!("Got signal 'Stop'");
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_completed_signal(&mut self, game_id: String) {
|
||||
info!("Got signal 'Completed'");
|
||||
if let Some(interface) = &self.current_download_agent {
|
||||
// When if let chains are stabilised, combine these two statements
|
||||
if interface.id == game_id {
|
||||
info!("Popping consumed data");
|
||||
let download_agent = self.remove_and_cleanup_game(&game_id);
|
||||
let download_agent_lock = download_agent.lock().unwrap();
|
||||
|
||||
let version = download_agent_lock.version.clone();
|
||||
let install_dir = download_agent_lock.stored_manifest.base_path.clone().to_string_lossy().to_string();
|
||||
|
||||
drop(download_agent_lock);
|
||||
|
||||
if let Err(error) =
|
||||
on_game_complete(game_id, version, install_dir, &self.app_handle)
|
||||
{
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Error(
|
||||
GameDownloadError::Communication(error),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
|
||||
fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) {
|
||||
info!("Got signal Queue");
|
||||
let download_agent = Arc::new(Mutex::new(GameDownloadAgent::new(
|
||||
id.clone(),
|
||||
version,
|
||||
target_download_dir,
|
||||
self.sender.clone(),
|
||||
)));
|
||||
let download_agent_lock = download_agent.lock().unwrap();
|
||||
|
||||
let agent_status = GameDownloadStatus::Queued;
|
||||
let interface_data = GameDownloadAgentQueueStandin {
|
||||
id: id.clone(),
|
||||
status: Mutex::new(agent_status),
|
||||
progress: download_agent_lock.progress.clone(),
|
||||
};
|
||||
let version_name = download_agent_lock.version.clone();
|
||||
|
||||
drop(download_agent_lock);
|
||||
|
||||
self.download_agent_registry
|
||||
.insert(interface_data.id.clone(), download_agent);
|
||||
self.download_queue.append(interface_data);
|
||||
|
||||
self.set_game_status(id, |db, id| {
|
||||
db.games.transient_statuses.insert(
|
||||
id.to_string(),
|
||||
GameTransientStatus::Downloading { version_name },
|
||||
);
|
||||
});
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
}
|
||||
|
||||
fn manage_go_signal(&mut self) {
|
||||
if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current_download_agent.is_some() {
|
||||
info!("skipping go signal due to existing download job");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("current download queue: {:?}", self.download_queue.read());
|
||||
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||
info!("starting download for {}", agent_data.id.clone());
|
||||
let download_agent = self
|
||||
.download_agent_registry
|
||||
.get(&agent_data.id)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let download_agent_lock = download_agent.lock().unwrap();
|
||||
self.current_download_agent = Some(agent_data);
|
||||
// Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData
|
||||
let agent_data = self.current_download_agent.clone().unwrap();
|
||||
|
||||
let version_name = download_agent_lock.version.clone();
|
||||
|
||||
let progress_object = download_agent_lock.progress.clone();
|
||||
*self.progress.lock().unwrap() = Some(progress_object);
|
||||
|
||||
let active_control_flag = download_agent_lock.control_flag.clone();
|
||||
self.active_control_flag = Some(active_control_flag.clone());
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
drop(download_agent_lock);
|
||||
|
||||
info!("Spawning download");
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
*download_thread_lock = Some(spawn(move || {
|
||||
let mut download_agent_lock = download_agent.lock().unwrap();
|
||||
match download_agent_lock.download() {
|
||||
// Returns once we've exited the download
|
||||
// (not necessarily completed)
|
||||
// The download agent will fire the completed event for us
|
||||
Ok(_) => {}
|
||||
// If an error occurred while *starting* the download
|
||||
Err(err) => {
|
||||
error!("error while managing download: {}", err);
|
||||
sender.send(DownloadManagerSignal::Error(err)).unwrap();
|
||||
}
|
||||
};
|
||||
drop(download_agent_lock);
|
||||
}));
|
||||
|
||||
// Set status for games
|
||||
for queue_game in self.download_queue.read() {
|
||||
let mut status_handle = queue_game.status.lock().unwrap();
|
||||
if queue_game.id == agent_data.id {
|
||||
*status_handle = GameDownloadStatus::Downloading;
|
||||
} else {
|
||||
*status_handle = GameDownloadStatus::Queued;
|
||||
}
|
||||
drop(status_handle);
|
||||
}
|
||||
|
||||
// Set flags for download manager
|
||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
self.set_status(DownloadManagerStatus::Downloading);
|
||||
self.set_game_status(agent_data.id.clone(), |db, id| {
|
||||
db.games.transient_statuses.insert(
|
||||
id.to_string(),
|
||||
GameTransientStatus::Downloading { version_name },
|
||||
);
|
||||
});
|
||||
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
}
|
||||
fn manage_error_signal(&mut self, error: GameDownloadError) {
|
||||
let current_status = self.current_download_agent.clone().unwrap();
|
||||
|
||||
self.remove_and_cleanup_game(¤t_status.id); // Remove all the locks and shit
|
||||
|
||||
let mut lock = current_status.status.lock().unwrap();
|
||||
*lock = GameDownloadStatus::Error;
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
|
||||
let game_id = current_status.id.clone();
|
||||
self.set_game_status(game_id, |db_handle, id| {
|
||||
db_handle.games.transient_statuses.remove(id);
|
||||
});
|
||||
|
||||
self.sender.send(DownloadManagerSignal::Update).unwrap();
|
||||
}
|
||||
fn manage_cancel_signal(&mut self) {
|
||||
self.stop_and_wait_current_download();
|
||||
|
||||
info!("cancel waited for download to finish");
|
||||
|
||||
self.cleanup_current_download();
|
||||
}
|
||||
fn set_status(&self, status: DownloadManagerStatus) {
|
||||
*self.status.lock().unwrap() = status;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
pub mod download_agent;
|
||||
pub mod download_commands;
|
||||
mod download_logic;
|
||||
pub mod download_manager;
|
||||
pub mod download_manager_builder;
|
||||
mod download_thread_control_flag;
|
||||
mod manifest;
|
||||
mod progress_object;
|
||||
pub mod queue;
|
||||
mod stored_manifest;
|
||||
404
src-tauri/src/games/downloads/download_agent.rs
Normal file
404
src-tauri/src/games/downloads/download_agent.rs
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
|
||||
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
|
||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::games::library::{on_game_complete, push_game_update};
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{create_dir_all, remove_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::spawn;
|
||||
use std::time::Instant;
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::stored_manifest::StoredManifest;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
completed_contexts: Mutex<VecDeque<usize>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
status: Mutex<DownloadStatus>
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub fn new(
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
let base_dir_path = Path::new(&base_dir);
|
||||
let data_base_dir_path = base_dir_path.join(id.clone());
|
||||
|
||||
let stored_manifest =
|
||||
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
completed_contexts: Mutex::new(VecDeque::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
info!("Ensured manifest exists");
|
||||
|
||||
self.ensure_contexts()?;
|
||||
info!("Ensured contexts exists");
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
info!("Setting up download");
|
||||
self.setup_download()?;
|
||||
info!("Setting progress object params");
|
||||
self.set_progress_object_params();
|
||||
info!("Running");
|
||||
let timer = Instant::now();
|
||||
push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
|
||||
let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
|
||||
info!(
|
||||
"{} took {}ms to download",
|
||||
self.id,
|
||||
timer.elapsed().as_millis()
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.manifest.lock().unwrap().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_manifest()
|
||||
}
|
||||
|
||||
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
let manifest_url = base_url
|
||||
.join(
|
||||
format!(
|
||||
"/api/v1/client/metadata/manifest?id={}&version={}",
|
||||
self.id,
|
||||
encode(&self.version)
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::ManifestDownloadFailed(
|
||||
response.status(),
|
||||
response.text().unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ApplicationDownloadError::Lock)
|
||||
}
|
||||
|
||||
fn set_progress_object_params(&self) {
|
||||
// Avoid re-setting it
|
||||
if self.progress.get_max() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
|
||||
let length = contexts.len();
|
||||
|
||||
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
|
||||
|
||||
debug!("Setting ProgressObject max to {}", chunk_count);
|
||||
self.progress.set_max(chunk_count);
|
||||
debug!("Setting ProgressObject size to {}", length);
|
||||
self.progress.set_size(length);
|
||||
debug!("Setting ProgressObject time to now");
|
||||
self.progress.set_time_now();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if !self.contexts.lock().unwrap().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.generate_contexts()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
|
||||
{
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
completed_contexts_lock.clear();
|
||||
completed_contexts_lock
|
||||
.extend(self.stored_manifest.get_completed_contexts());
|
||||
}
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
let path = base_path.join(Path::new(&raw_path));
|
||||
|
||||
let container = path.parent().unwrap();
|
||||
create_dir_all(container).unwrap();
|
||||
|
||||
let file = File::create(path.clone()).unwrap();
|
||||
let mut running_offset = 0;
|
||||
|
||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||
contexts.push(DropDownloadContext {
|
||||
file_name: raw_path.to_string(),
|
||||
version: chunk.version_name.to_string(),
|
||||
offset: running_offset,
|
||||
index,
|
||||
game_id: game_id.to_string(),
|
||||
path: path.clone(),
|
||||
checksum: chunk.checksums[index].clone(),
|
||||
length: *length,
|
||||
permissions: chunk.permissions,
|
||||
});
|
||||
running_offset += *length as u64;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
*self.contexts.lock().unwrap() = contexts;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
info!("downloading game: {}", self.id);
|
||||
const DOWNLOAD_MAX_THREADS: usize = 1;
|
||||
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(DOWNLOAD_MAX_THREADS)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
pool.scope(|scope| {
|
||||
let client = &reqwest::blocking::Client::new();
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let client = client.clone();
|
||||
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||
|
||||
let progress = self.progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
// If we've done this one already, skip it
|
||||
if self.completed_contexts.lock().unwrap().contains(&index) {
|
||||
progress_handle.add(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let request = generate_request(&base_url, client, &context);
|
||||
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request) {
|
||||
Ok(res) => {
|
||||
if res {
|
||||
completed_indexes.push(index);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
info!("Completed context id {}", index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newly_completed = completed_indexes.to_owned();
|
||||
|
||||
|
||||
let completed_lock_len = {
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
for (item, _) in newly_completed.iter() {
|
||||
completed_contexts_lock.push_front(item);
|
||||
}
|
||||
|
||||
completed_contexts_lock.len()
|
||||
};
|
||||
|
||||
info!("Got newly completed");
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != contexts.len() {
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into());
|
||||
info!("Setting completed contexts");
|
||||
self.stored_manifest.write();
|
||||
info!("Wrote completed contexts");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
info!("Sending completed signal");
|
||||
|
||||
|
||||
// We've completed
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Completed(self.metadata()))
|
||||
.unwrap();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_request(base_url: &url::Url, client: reqwest::blocking::Client, context: &DropDownloadContext) -> reqwest::blocking::RequestBuilder {
|
||||
let chunk_url = base_url
|
||||
.join(&format!(
|
||||
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
|
||||
// Encode the parts we don't trust
|
||||
context.game_id,
|
||||
encode(&context.version),
|
||||
encode(&context.file_name),
|
||||
context.index
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let request = client
|
||||
.get(chunk_url)
|
||||
.header("Authorization", header);
|
||||
request
|
||||
}
|
||||
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
self.download(app_handle)
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
self.progress.clone()
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
self.control_flag.clone()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> DownloadableMetadata {
|
||||
DownloadableMetadata {
|
||||
id: self.id.clone(),
|
||||
version: Some(self.version.clone()),
|
||||
download_type: DownloadType::Game,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||
app_handle
|
||||
.emit("download_error", error.to_string())
|
||||
.unwrap();
|
||||
|
||||
error!("error while managing download: {}", error);
|
||||
|
||||
set_game_status(app_handle, self.metadata(), |db_handle, meta| {
|
||||
db_handle.applications.transient_statuses.remove(meta);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap();
|
||||
}
|
||||
|
||||
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_game(
|
||||
|
|
@ -9,11 +11,15 @@ pub fn download_game(
|
|||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(
|
||||
Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box<dyn Downloadable + Send + Sync>
|
||||
);
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_game(game_id, game_version, install_dir)
|
||||
.queue_download(game_download_agent)
|
||||
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
|
||||
}
|
||||
|
||||
|
|
@ -41,8 +47,8 @@ pub fn move_game_in_queue(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, game_id: String) {
|
||||
state.lock().unwrap().download_manager.cancel(game_id)
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::progress_object::ProgressHandle;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::{DropServerError, RemoteAccessError};
|
||||
use crate::DB;
|
||||
use log::warn;
|
||||
use log::{error, info, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::Response;
|
||||
use tauri::utils::acl::Permission;
|
||||
use reqwest::blocking::{Client, Request, RequestBuilder, Response};
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
|
|
@ -21,10 +22,6 @@ use std::{
|
|||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
use super::download_agent::GameDownloadError;
|
||||
use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use super::progress_object::ProgressHandle;
|
||||
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
destination: W,
|
||||
|
|
@ -68,18 +65,18 @@ impl Seek for DropWriter<File> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DropDownloadPipeline<R: Read, W: Write> {
|
||||
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
||||
pub source: R,
|
||||
pub destination: DropWriter<W>,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
pub control_flag: &'a DownloadThreadControl,
|
||||
pub progress: ProgressHandle,
|
||||
pub size: usize,
|
||||
}
|
||||
impl DropDownloadPipeline<Response, File> {
|
||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||
fn new(
|
||||
source: Response,
|
||||
destination: DropWriter<File>,
|
||||
control_flag: DownloadThreadControl,
|
||||
control_flag: &'a DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
size: usize,
|
||||
) -> Self {
|
||||
|
|
@ -124,41 +121,24 @@ impl DropDownloadPipeline<Response, File> {
|
|||
}
|
||||
|
||||
pub fn download_game_chunk(
|
||||
ctx: DropDownloadContext,
|
||||
control_flag: DownloadThreadControl,
|
||||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
) -> Result<bool, GameDownloadError> {
|
||||
request: RequestBuilder
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let chunk_url = base_url
|
||||
.join(&format!(
|
||||
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
|
||||
// Encode the parts we don't trust
|
||||
ctx.game_id,
|
||||
encode(&ctx.version),
|
||||
encode(&ctx.file_name),
|
||||
ctx.index
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let response = client
|
||||
.get(chunk_url)
|
||||
.header("Authorization", header)
|
||||
let response = request
|
||||
.send()
|
||||
.map_err(|e| GameDownloadError::Communication(e.into()))?;
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
warn!("{}", response.text().unwrap());
|
||||
return Err(GameDownloadError::Communication(
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidCodeError(400),
|
||||
));
|
||||
}
|
||||
|
|
@ -173,8 +153,9 @@ pub fn download_game_chunk(
|
|||
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
return Err(GameDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse,
|
||||
error!("Recieved 0 length content from server");
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(response.json::<DropServerError>().unwrap()),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +167,9 @@ pub fn download_game_chunk(
|
|||
content_length.unwrap().try_into().unwrap(),
|
||||
);
|
||||
|
||||
let completed = pipeline.copy().map_err(GameDownloadError::IoError)?;
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
if !completed {
|
||||
return Ok(false);
|
||||
};
|
||||
|
|
@ -195,7 +178,7 @@ pub fn download_game_chunk(
|
|||
#[cfg(unix)]
|
||||
{
|
||||
let permissions = Permissions::from_mode(ctx.permissions);
|
||||
set_permissions(ctx.path, permissions).unwrap();
|
||||
set_permissions(ctx.path.clone(), permissions).unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
5
src-tauri/src/games/downloads/mod.rs
Normal file
5
src-tauri/src/games/downloads/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod download_agent;
|
||||
pub mod download_commands;
|
||||
mod download_logic;
|
||||
mod manifest;
|
||||
mod stored_manifest;
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
use std::{
|
||||
default,
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binary::binary_stream::Endian;
|
||||
|
||||
|
|
@ -44,15 +43,15 @@ impl StoredManifest {
|
|||
}
|
||||
};
|
||||
|
||||
let manifest = match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
|
||||
|
||||
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
StoredManifest::new(game_id, game_version, base_path)
|
||||
}
|
||||
};
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
|
||||
|
|
@ -73,8 +72,8 @@ impl StoredManifest {
|
|||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &Mutex<Vec<usize>>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.lock().unwrap().clone();
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &Vec<usize>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.clone();
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||
self.completed_contexts.lock().unwrap().clone()
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
use std::fs::remove_dir_all;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Emitter;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
|
||||
use crate::db::GameVersion;
|
||||
use crate::db::{GameStatus, GameTransientStatus};
|
||||
use crate::downloads::download_manager::GameDownloadStatus;
|
||||
use crate::download_manager::download_manager::DownloadStatus;
|
||||
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
||||
use crate::process::process_manager::Platform;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::{auth::generate_authorization_header, AppState, DB};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
|
|
@ -37,13 +40,13 @@ pub struct Game {
|
|||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct GameUpdateEvent {
|
||||
pub game_id: String,
|
||||
pub status: (Option<GameStatus>, Option<GameTransientStatus>),
|
||||
pub status: (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct QueueUpdateEventQueueData {
|
||||
pub id: String,
|
||||
pub status: GameDownloadStatus,
|
||||
pub meta: DownloadableMetadata,
|
||||
pub status: DownloadStatus,
|
||||
pub progress: f64,
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +55,12 @@ pub struct QueueUpdateEvent {
|
|||
pub queue: Vec<QueueUpdateEventQueueData>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct StatsUpdateEvent {
|
||||
pub speed: usize,
|
||||
pub time: usize,
|
||||
}
|
||||
|
||||
// Game version with some fields missing and size information
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -91,11 +100,11 @@ fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
|||
|
||||
for game in games.iter() {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
if !db_handle.games.statuses.contains_key(&game.id) {
|
||||
if !db_handle.applications.game_statuses.contains_key(&game.id) {
|
||||
db_handle
|
||||
.games
|
||||
.statuses
|
||||
.insert(game.id.clone(), GameStatus::Remote {});
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(game.id.clone(), GameDownloadStatus::Remote {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,10 +163,10 @@ fn fetch_game_logic(
|
|||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
|
||||
db_handle
|
||||
.games
|
||||
.statuses
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(id.clone())
|
||||
.or_insert(GameStatus::Remote {});
|
||||
.or_insert(GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
|
|
@ -171,8 +180,8 @@ fn fetch_game_logic(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
|
||||
let result = fetch_game_logic(id, app);
|
||||
pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
|
||||
let result = fetch_game_logic(game_id, app);
|
||||
|
||||
if result.is_err() {
|
||||
return Err(result.err().unwrap().to_string());
|
||||
|
|
@ -224,6 +233,89 @@ fn fetch_game_verion_options_logic<'a>(
|
|||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn uninstall_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
app_handle: AppHandle
|
||||
) -> Result<(), String> {
|
||||
let meta = get_current_meta(&game_id)?;
|
||||
println!("{:?}", meta);
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
println!("Triggered uninstall for agent");
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.entry(meta.clone())
|
||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&meta,
|
||||
(None, Some(ApplicationTransientStatus::Uninstalling {})),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
if previous_state.is_none() {
|
||||
info!("uninstall job doesn't have previous state, failing silently");
|
||||
return;
|
||||
}
|
||||
let previous_state = previous_state.unwrap();
|
||||
if let Some((version_name, install_dir)) = match previous_state {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.entry(meta.clone())
|
||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||
drop(db_handle);
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
spawn(move || match remove_dir_all(install_dir) {
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
|
||||
info!("uninstalled game id {}", &meta.id);
|
||||
|
||||
push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_meta(game_id: &String) -> Result<DownloadableMetadata, String> {
|
||||
match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => Ok(meta.clone()),
|
||||
None => Err(String::from("Could not find installed version")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_verion_options<'a>(
|
||||
game_id: String,
|
||||
|
|
@ -233,19 +325,19 @@ pub fn fetch_game_verion_options<'a>(
|
|||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
game_id: String,
|
||||
version_name: String,
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
let base_url = DB.fetch_base_url();
|
||||
if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound) }
|
||||
|
||||
let endpoint = base_url.join(
|
||||
format!(
|
||||
"/api/v1/client/metadata/version?id={}&version={}",
|
||||
game_id,
|
||||
encode(&version_name)
|
||||
meta.id,
|
||||
encode(meta.version.as_ref().unwrap())
|
||||
)
|
||||
.as_str(),
|
||||
)?;
|
||||
|
|
@ -261,38 +353,43 @@ pub fn on_game_complete(
|
|||
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle
|
||||
.games
|
||||
.versions
|
||||
.entry(game_id.clone())
|
||||
.applications
|
||||
.game_versions
|
||||
.entry(meta.id.clone())
|
||||
.or_default()
|
||||
.insert(version_name.clone(), data.clone());
|
||||
.insert(meta.version.clone().unwrap(), data.clone());
|
||||
handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
|
||||
let status = if data.setup_command.is_empty() {
|
||||
GameStatus::Installed {
|
||||
version_name,
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
}
|
||||
} else {
|
||||
GameStatus::SetupRequired {
|
||||
version_name,
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
}
|
||||
};
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle
|
||||
.games
|
||||
.statuses
|
||||
.insert(game_id.clone(), status.clone());
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", game_id),
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id,
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(status), None),
|
||||
},
|
||||
)
|
||||
|
|
@ -300,3 +397,15 @@ pub fn on_game_complete(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) {
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
3
src-tauri/src/games/mod.rs
Normal file
3
src-tauri/src/games/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod downloads;
|
||||
pub mod library;
|
||||
pub mod state;
|
||||
32
src-tauri/src/games/state.rs
Normal file
32
src-tauri/src/games/state.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use crate::{
|
||||
db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB
|
||||
};
|
||||
|
||||
pub type GameStatusWithTransient = (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>);
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock
|
||||
.applications
|
||||
.transient_statuses
|
||||
.get(meta)
|
||||
.cloned(),
|
||||
None => None,
|
||||
};
|
||||
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
|
||||
drop(db_lock);
|
||||
|
||||
if online_state.is_some() {
|
||||
return (None, online_state);
|
||||
}
|
||||
|
||||
if offline_state.is_some() {
|
||||
return (offline_state, None);
|
||||
}
|
||||
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,58 @@
|
|||
mod auth;
|
||||
mod db;
|
||||
mod downloads;
|
||||
mod library;
|
||||
mod games;
|
||||
|
||||
mod autostart;
|
||||
mod cleanup;
|
||||
mod debug;
|
||||
mod process;
|
||||
mod remote;
|
||||
mod state;
|
||||
mod tools;
|
||||
pub mod download_manager;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod cleanup;
|
||||
|
||||
use crate::autostart::{get_autostart_enabled, toggle_autostart};
|
||||
use crate::db::DatabaseImpls;
|
||||
use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect};
|
||||
use auth::{
|
||||
auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake,
|
||||
retry_connect, sign_out,
|
||||
};
|
||||
use cleanup::{cleanup_and_exit, quit};
|
||||
use db::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus,
|
||||
DATA_ROOT_DIR,
|
||||
};
|
||||
use downloads::download_commands::*;
|
||||
use downloads::download_manager::DownloadManager;
|
||||
use downloads::download_manager_builder::DownloadManagerBuilder;
|
||||
use debug::fetch_system_data;
|
||||
use download_manager::download_manager::DownloadManager;
|
||||
use download_manager::download_manager_builder::DownloadManagerBuilder;
|
||||
use games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads};
|
||||
use http::Response;
|
||||
use http::{header::*, response::Builder as ResponseBuilder};
|
||||
use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game};
|
||||
use log::{debug, info, LevelFilter};
|
||||
use games::library::{
|
||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game
|
||||
};
|
||||
use log::{debug, info, warn, LevelFilter};
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::append::rolling_file::RollingFileAppender;
|
||||
use log4rs::config::{Appender, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log4rs::Config;
|
||||
use process::process_commands::launch_game;
|
||||
use process::compat::CompatibilityManager;
|
||||
use process::process_commands::{kill_game, launch_game};
|
||||
use process::process_manager::ProcessManager;
|
||||
use remote::{gen_drop_url, use_remote};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem};
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||
use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
|
|
@ -65,7 +77,7 @@ pub struct User {
|
|||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppState {
|
||||
pub struct AppState<'a> {
|
||||
status: AppStatus,
|
||||
user: Option<User>,
|
||||
games: HashMap<String, Game>,
|
||||
|
|
@ -73,18 +85,20 @@ pub struct AppState {
|
|||
#[serde(skip_serializing)]
|
||||
download_manager: Arc<DownloadManager>,
|
||||
#[serde(skip_serializing)]
|
||||
process_manager: Arc<Mutex<ProcessManager>>,
|
||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||
#[serde(skip_serializing)]
|
||||
compat_manager: Arc<Mutex<CompatibilityManager>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<AppState, String> {
|
||||
fn fetch_state(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<String, String> {
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = guard.clone();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
|
||||
fn setup(handle: AppHandle) -> AppState {
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
|
||||
.append(false)
|
||||
|
|
@ -110,8 +124,9 @@ fn setup(handle: AppHandle) -> AppState {
|
|||
log4rs::init_config(config).unwrap();
|
||||
|
||||
let games = HashMap::new();
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||
let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new()));
|
||||
|
||||
debug!("Checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
|
@ -122,27 +137,80 @@ fn setup(handle: AppHandle) -> AppState {
|
|||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
};
|
||||
}
|
||||
|
||||
debug!("Database is set up");
|
||||
|
||||
let (app_status, user) = auth::setup().unwrap();
|
||||
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
for (game_id, status) in statuses.into_iter() {
|
||||
match status {
|
||||
db::GameDownloadStatus::Remote {} => {}
|
||||
db::GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
db::GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
} => {
|
||||
let install_dir_path = Path::new(&install_dir);
|
||||
if !install_dir_path.exists() {
|
||||
missing_games.push(game_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("detected games missing: {:?}", missing_games);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(game_id)
|
||||
.and_modify(|v| *v = GameDownloadStatus::Remote {});
|
||||
}
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
|
||||
info!("finished setup!");
|
||||
|
||||
// Sync autostart state
|
||||
if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
|
||||
warn!("Failed to sync autostart state: {}", e);
|
||||
}
|
||||
|
||||
AppState {
|
||||
status: app_status,
|
||||
user,
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_manager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init());
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[allow(unused_variables)]
|
||||
|
|
@ -152,15 +220,18 @@ pub fn run() {
|
|||
}));
|
||||
}
|
||||
|
||||
let mut app = builder
|
||||
let app = builder
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Core utils
|
||||
fetch_state,
|
||||
quit,
|
||||
fetch_system_data,
|
||||
// Auth
|
||||
auth_initiate,
|
||||
retry_connect,
|
||||
manual_recieve_handshake,
|
||||
sign_out,
|
||||
// Remote
|
||||
use_remote,
|
||||
gen_drop_url,
|
||||
|
|
@ -178,11 +249,19 @@ pub fn run() {
|
|||
pause_game_downloads,
|
||||
resume_game_downloads,
|
||||
cancel_game,
|
||||
uninstall_game,
|
||||
// Processes
|
||||
launch_game,
|
||||
kill_game,
|
||||
toggle_autostart,
|
||||
get_autostart_enabled,
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
Some(vec!["--minimize"]),
|
||||
))
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
|
|
@ -253,6 +332,18 @@ pub fn run() {
|
|||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
|
||||
{
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!("Database corrupted. Original file at {}", original.canonicalize().unwrap().to_string_lossy().to_string());
|
||||
app.dialog()
|
||||
.message("Database corrupted. A copy has been saved at: ".to_string() + original.to_str().unwrap())
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
||||
|
|
@ -272,8 +363,16 @@ pub fn run() {
|
|||
let response = client
|
||||
.get(object_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
.send();
|
||||
if response.is_err() {
|
||||
warn!(
|
||||
"failed to fetch object with error: {}",
|
||||
response.err().unwrap()
|
||||
);
|
||||
responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
|
||||
return;
|
||||
}
|
||||
let response = response.unwrap();
|
||||
|
||||
let resp_builder = ResponseBuilder::new().header(
|
||||
CONTENT_TYPE,
|
||||
|
|
@ -284,22 +383,20 @@ pub fn run() {
|
|||
|
||||
responder.respond(resp);
|
||||
})
|
||||
.on_window_event(|window, event| match event {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|app_handle, event| match event {
|
||||
RunEvent::ExitRequested { code, api, .. } => {
|
||||
app.run(|app_handle, event| {
|
||||
if let RunEvent::ExitRequested { code, api, .. } = event {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
51
src-tauri/src/process/compat.rs
Normal file
51
src-tauri/src/process/compat.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::{
|
||||
fs::create_dir_all,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use crate::db::DATA_ROOT_DIR;
|
||||
|
||||
pub struct CompatibilityManager {
|
||||
compat_tools_path: PathBuf,
|
||||
prefixes_path: PathBuf,
|
||||
created_paths: AtomicBool,
|
||||
}
|
||||
|
||||
/*
|
||||
This gets built into both the Windows & Linux client, but
|
||||
we only need it in the Linux client. Therefore, it should
|
||||
do nothing but take a little bit of memory if we're on
|
||||
Windows.
|
||||
*/
|
||||
impl CompatibilityManager {
|
||||
pub fn new() -> Self {
|
||||
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
let compat_tools_path = root_dir_lock.join("compatibility_tools");
|
||||
let prefixes_path = root_dir_lock.join("prefixes");
|
||||
drop(root_dir_lock);
|
||||
|
||||
Self {
|
||||
compat_tools_path,
|
||||
prefixes_path,
|
||||
created_paths: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_paths_exist(&self) -> Result<(), String> {
|
||||
if self.created_paths.fetch_and(true, Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
if !self.compat_tools_path.exists() {
|
||||
create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if !self.prefixes_path.exists() {
|
||||
create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.created_paths.store(true, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod compat;
|
||||
pub mod process_commands;
|
||||
pub mod process_manager;
|
||||
pub mod process_commands;
|
||||
|
|
@ -1,16 +1,40 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, games::library::get_current_meta, AppState, DB};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex<AppState>>) -> Result<(), String> {
|
||||
pub fn launch_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
process_manager_lock.launch_game(game_id)?;
|
||||
let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() {
|
||||
Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name,
|
||||
Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")),
|
||||
_ => return Err(String::from("Game not installed"))
|
||||
};
|
||||
|
||||
|
||||
let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game };
|
||||
|
||||
|
||||
process_manager_lock.launch_process(meta)?;
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let meta = get_current_meta(&game_id)?;
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
process_manager_lock.kill_game(meta).map_err(|x| x.to_string())
|
||||
}
|
||||
|
|
@ -1,28 +1,33 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Stdout, Write},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command},
|
||||
sync::LazyLock,
|
||||
process::{Child, Command, ExitStatus},
|
||||
sync::{Arc, Mutex},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_child::SharedChild;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
|
||||
|
||||
use crate::{
|
||||
db::{GameStatus, DATA_ROOT_DIR},
|
||||
DB,
|
||||
db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::state::GameStatusManager, AppState, DB
|
||||
};
|
||||
|
||||
pub struct ProcessManager {
|
||||
pub struct ProcessManager<'a> {
|
||||
current_platform: Platform,
|
||||
log_output_dir: PathBuf,
|
||||
processes: HashMap<String, Child>,
|
||||
processes: HashMap<DownloadableMetadata, Arc<SharedChild>>,
|
||||
app_handle: AppHandle,
|
||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||
}
|
||||
|
||||
impl ProcessManager {
|
||||
pub fn new() -> Self {
|
||||
impl ProcessManager<'_> {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
let log_output_dir = root_dir_lock.join("logs");
|
||||
drop(root_dir_lock);
|
||||
|
|
@ -34,119 +39,311 @@ impl ProcessManager {
|
|||
Platform::Linux
|
||||
},
|
||||
|
||||
app_handle,
|
||||
processes: HashMap::new(),
|
||||
log_output_dir,
|
||||
game_launchers: HashMap::from([
|
||||
// Current platform to target platform
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Linux),
|
||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Windows),
|
||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_command(&self, install_dir: &String, raw_command: String) -> (String, Vec<String>) {
|
||||
let command_components = raw_command.split(" ").collect::<Vec<&str>>();
|
||||
let root = command_components[0].to_string();
|
||||
// There's no easy way to distinguish between an executable name with
|
||||
// spaces and it's arguments.
|
||||
// I think if we just join the install_dir to whatever the user provides us, we'll be alright
|
||||
// In future, we should have a separate field for executable name and it's arguments
|
||||
fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec<String>) {
|
||||
// let command_components = raw_command.split(" ").collect::<Vec<&str>>();
|
||||
let root = raw_command;
|
||||
|
||||
let install_dir = Path::new(install_dir);
|
||||
let absolute_exe = install_dir.join(root);
|
||||
|
||||
/*
|
||||
let args = command_components[1..]
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
(absolute_exe.to_str().unwrap().to_owned(), args)
|
||||
*/
|
||||
(absolute_exe, Vec::new())
|
||||
}
|
||||
pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> {
|
||||
return match self.processes.get(&meta) {
|
||||
Some(child) => {
|
||||
child.kill()?;
|
||||
child.wait()?;
|
||||
Ok(())
|
||||
},
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"Game ID not running",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result<ExitStatus, std::io::Error>) {
|
||||
if !self.processes.contains_key(&meta) {
|
||||
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
info!("process for {:?} exited with {:?}", meta, result);
|
||||
|
||||
self.processes.remove(&meta);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
if let Some(saved_state) = current_state {
|
||||
if let GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} = saved_state
|
||||
{
|
||||
if let Ok(exit_code) = result {
|
||||
if exit_code.success() {
|
||||
db_handle.applications.game_statuses.insert(
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: version_name.to_string(),
|
||||
install_dir: install_dir.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
push_game_update(&self.app_handle, &meta, status);
|
||||
|
||||
// TODO better management
|
||||
}
|
||||
|
||||
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
||||
let current = &self.current_platform;
|
||||
let valid_platforms = PROCESS_COMPATABILITY_MATRIX
|
||||
.get(current)
|
||||
.ok_or("Incomplete platform compatability matrix.")?;
|
||||
|
||||
Ok(valid_platforms.contains(platform))
|
||||
Ok(self
|
||||
.game_launchers
|
||||
.contains_key(&(current.clone(), platform.clone())))
|
||||
}
|
||||
|
||||
pub fn launch_game(&mut self, game_id: String) -> Result<(), String> {
|
||||
if self.processes.contains_key(&game_id) {
|
||||
pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> {
|
||||
if self.processes.contains_key(&meta) {
|
||||
return Err("Game or setup is already running.".to_owned());
|
||||
}
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions);
|
||||
|
||||
let game_status = db_lock
|
||||
.games
|
||||
.statuses
|
||||
.get(&game_id)
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&meta.id)
|
||||
.ok_or("Game not installed")?;
|
||||
|
||||
let GameStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} = game_status
|
||||
else {
|
||||
return Err("Game not installed.".to_owned());
|
||||
let status_metadata: Option<(&String, &String)> = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if status_metadata.is_none() {
|
||||
return Err("Game has not been downloaded.".to_owned());
|
||||
}
|
||||
|
||||
let (version_name, install_dir) = status_metadata.unwrap();
|
||||
|
||||
let game_version = db_lock
|
||||
.games
|
||||
.versions
|
||||
.get(&game_id)
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&meta.id)
|
||||
.ok_or("Invalid game ID".to_owned())?
|
||||
.get(version_name)
|
||||
.ok_or("Invalid version name".to_owned())?;
|
||||
|
||||
let (command, args) =
|
||||
self.process_command(install_dir, game_version.launch_command.clone());
|
||||
let raw_command: String = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.launch_command.clone(),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => game_version.setup_command.clone(),
|
||||
_ => panic!("unreachable code"),
|
||||
};
|
||||
|
||||
info!("launching process {} in {}", command, install_dir);
|
||||
let (command, args) = self.process_command(install_dir, raw_command);
|
||||
|
||||
let target_current_dir = command.parent().unwrap().to_str().unwrap();
|
||||
|
||||
info!(
|
||||
"launching process {} in {}",
|
||||
command.to_str().unwrap(),
|
||||
target_current_dir
|
||||
);
|
||||
|
||||
let current_time = chrono::offset::Local::now();
|
||||
let mut log_file = OpenOptions::new()
|
||||
let log_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(
|
||||
self.log_output_dir
|
||||
.join(format!("{}-{}.log", game_id, current_time.timestamp())),
|
||||
.join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())),
|
||||
)
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
let mut error_file = OpenOptions::new()
|
||||
let error_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(
|
||||
self.log_output_dir
|
||||
.join(format!("{}-{}-error.log", game_id, current_time.timestamp())),
|
||||
)
|
||||
.open(self.log_output_dir.join(format!(
|
||||
"{}-{}-{}-error.log",
|
||||
meta.id.clone(),
|
||||
meta.version.clone().unwrap_or_default(),
|
||||
current_time.timestamp()
|
||||
)))
|
||||
.map_err(|v| v.to_string())?;
|
||||
|
||||
info!("opened log file for {}", command);
|
||||
let current_platform = self.current_platform.clone();
|
||||
let target_platform = game_version.platform.clone();
|
||||
|
||||
let launch_process = Command::new(command)
|
||||
.current_dir(install_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.map_err(|v| v.to_string())?;
|
||||
let game_launcher = self
|
||||
.game_launchers
|
||||
.get(&(current_platform, target_platform))
|
||||
.ok_or("Invalid version for this platform.")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
self.processes.insert(game_id, launch_process);
|
||||
let launch_process = game_launcher.launch_process(
|
||||
&meta,
|
||||
command.to_str().unwrap().to_owned(),
|
||||
args,
|
||||
&target_current_dir.to_string(),
|
||||
log_file,
|
||||
error_file,
|
||||
)?;
|
||||
|
||||
let launch_process_handle =
|
||||
Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?);
|
||||
|
||||
db_lock
|
||||
.applications
|
||||
.transient_statuses
|
||||
.insert(meta.clone(), ApplicationTransientStatus::Running {});
|
||||
|
||||
push_game_update(
|
||||
&self.app_handle,
|
||||
&meta,
|
||||
(None, Some(ApplicationTransientStatus::Running {})),
|
||||
);
|
||||
|
||||
let wait_thread_handle = launch_process_handle.clone();
|
||||
let wait_thread_apphandle = self.app_handle.clone();
|
||||
let wait_thread_game_id = meta.clone();
|
||||
|
||||
spawn(move || {
|
||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||
|
||||
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||
let app_state_handle = app_state.lock().unwrap();
|
||||
|
||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id, result);
|
||||
|
||||
// As everything goes out of scope, they should get dropped
|
||||
// But just to explicit about it
|
||||
drop(process_manager_handle);
|
||||
drop(app_state_handle);
|
||||
});
|
||||
|
||||
self.processes.insert(meta, wait_thread_handle);
|
||||
|
||||
info!("finished spawning process");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
|
||||
pub type ProcessCompatabilityMatrix = HashMap<Platform, Vec<Platform>>;
|
||||
pub static PROCESS_COMPATABILITY_MATRIX: LazyLock<ProcessCompatabilityMatrix> =
|
||||
LazyLock::new(|| {
|
||||
let mut matrix: ProcessCompatabilityMatrix = HashMap::new();
|
||||
pub trait ProcessHandler: Send + 'static {
|
||||
fn launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String>;
|
||||
}
|
||||
|
||||
matrix.insert(Platform::Windows, vec![Platform::Windows]);
|
||||
matrix.insert(Platform::Linux, vec![Platform::Linux]); // TODO: add Proton support
|
||||
struct NativeGameLauncher;
|
||||
impl ProcessHandler for NativeGameLauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String> {
|
||||
Command::new(command)
|
||||
.current_dir(current_dir)
|
||||
.stdout(log_file)
|
||||
.stderr(error_file)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.map_err(|v| v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
});
|
||||
const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||
struct UMULauncher;
|
||||
impl ProcessHandler for UMULauncher {
|
||||
fn launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
current_dir: &String,
|
||||
log_file: File,
|
||||
error_file: File,
|
||||
) -> Result<Child, String> {
|
||||
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
|
||||
.game_id(String::from("0"))
|
||||
.launch_args(args)
|
||||
.build()
|
||||
.spawn()
|
||||
.map_err(|x| x.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use http::StatusCode;
|
||||
use log::{info, warn};
|
||||
use reqwest::blocking::Response;
|
||||
use serde::Deserialize;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
|
|
@ -16,31 +18,44 @@ pub enum RemoteAccessError {
|
|||
ParsingError(ParseError),
|
||||
InvalidCodeError(u16),
|
||||
InvalidEndpoint,
|
||||
HandshakeFailed,
|
||||
HandshakeFailed(String),
|
||||
GameNotFound,
|
||||
InvalidResponse,
|
||||
InvalidResponse(DropServerError),
|
||||
InvalidRedirect,
|
||||
ManifestDownloadFailed(StatusCode, String),
|
||||
OutOfSync,
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RemoteAccessError::FetchError(error) => write!(f, "{}", error),
|
||||
RemoteAccessError::FetchError(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|e| e.to_string())
|
||||
.or_else(|| Some("Unknown error".to_string()))
|
||||
.unwrap()
|
||||
),
|
||||
RemoteAccessError::ParsingError(parse_error) => {
|
||||
write!(f, "{}", parse_error)
|
||||
}
|
||||
RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
|
||||
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
|
||||
RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"),
|
||||
RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
|
||||
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
|
||||
RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"),
|
||||
RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
|
||||
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
|
||||
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
|
||||
f,
|
||||
"Failed to download game manifest: {} {}",
|
||||
status, response
|
||||
),
|
||||
RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
|
||||
RemoteAccessError::Generic(message) => write!(f, "{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +78,15 @@ impl From<u16> for RemoteAccessError {
|
|||
|
||||
impl std::error::Error for RemoteAccessError {}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropServerError {
|
||||
pub status_code: usize,
|
||||
pub status_message: String,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
|
|
@ -71,7 +95,7 @@ struct DropHealthcheck {
|
|||
|
||||
async fn use_remote_logic<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
|
@ -103,7 +127,7 @@ async fn use_remote_logic<'a>(
|
|||
#[tauri::command]
|
||||
pub async fn use_remote<'a>(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'a>>>,
|
||||
) -> Result<(), String> {
|
||||
let result = use_remote_logic(url, state).await;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
db::{GameStatus, GameTransientStatus},
|
||||
DB,
|
||||
};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameStatus>,
|
||||
Option<GameTransientStatus>,
|
||||
);
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let offline_state = db_lock.games.statuses.get(game_id).cloned();
|
||||
let online_state = db_lock.games.transient_statuses.get(game_id).cloned();
|
||||
drop(db_lock);
|
||||
|
||||
if online_state.is_some() {
|
||||
return (None, online_state);
|
||||
}
|
||||
|
||||
if offline_state.is_some() {
|
||||
return (offline_state, None);
|
||||
}
|
||||
|
||||
return (None, None);
|
||||
}
|
||||
}
|
||||
3
src-tauri/src/tools/compatibility_layer.rs
Normal file
3
src-tauri/src/tools/compatibility_layer.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub struct CompatibilityLayer {
|
||||
|
||||
}
|
||||
4
src-tauri/src/tools/mod.rs
Normal file
4
src-tauri/src/tools/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod prefix;
|
||||
mod registry;
|
||||
mod tool;
|
||||
mod compatibility_layer;
|
||||
0
src-tauri/src/tools/prefix.rs
Normal file
0
src-tauri/src/tools/prefix.rs
Normal file
7
src-tauri/src/tools/registry.rs
Normal file
7
src-tauri/src/tools/registry.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
|
||||
pub struct Registry<T: Downloadable> {
|
||||
tools: HashMap<String, T>
|
||||
}
|
||||
54
src-tauri/src/tools/tool.rs
Normal file
54
src-tauri/src/tools/tool.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject};
|
||||
|
||||
pub struct ToolDownloadAgent {
|
||||
id: String,
|
||||
version: String,
|
||||
location: String,
|
||||
control_flag: DownloadThreadControl,
|
||||
progress: Arc<ProgressObject>,
|
||||
}
|
||||
impl Downloadable for ToolDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn status(&self) -> crate::download_manager::download_manager::DownloadStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> DownloadableMetadata {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_initialised(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: crate::download_manager::application_download_error::ApplicationDownloadError) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"externalBin": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
types.ts
14
types.ts
|
|
@ -52,9 +52,23 @@ export enum GameStatusEnum {
|
|||
Updating = "Updating",
|
||||
Uninstalling = "Uninstalling",
|
||||
SetupRequired = "SetupRequired",
|
||||
Running = "Running"
|
||||
}
|
||||
|
||||
export type GameStatus = {
|
||||
type: GameStatusEnum;
|
||||
version_name?: string;
|
||||
};
|
||||
|
||||
export enum DownloadableType {
|
||||
Game = "Game",
|
||||
Tool = "Tool",
|
||||
DLC = "DLC",
|
||||
Mod = "Mod"
|
||||
}
|
||||
|
||||
export type DownloadableMetadata = {
|
||||
id: string,
|
||||
version: string,
|
||||
downloadType: DownloadableType
|
||||
}
|
||||
9
utils/generateGameMeta.ts
Normal file
9
utils/generateGameMeta.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { type DownloadableMetadata, DownloadableType } from '~/types'
|
||||
|
||||
export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
|
||||
return {
|
||||
id: gameId,
|
||||
version,
|
||||
downloadType: DownloadableType.Game
|
||||
}
|
||||
}
|
||||
12
yarn.lock
12
yarn.lock
|
|
@ -1392,6 +1392,13 @@
|
|||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-os@~2":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"
|
||||
integrity sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-shell@>=2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz#b6fc88ab070fd5f620e46405715779aa44eb8428"
|
||||
|
|
@ -3928,11 +3935,6 @@ mlly@^1.3.0, mlly@^1.4.2, mlly@^1.6.1, mlly@^1.7.1:
|
|||
pkg-types "^1.2.0"
|
||||
ufo "^1.5.4"
|
||||
|
||||
moment@^2.30.1:
|
||||
version "2.30.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
|
||||
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
|
||||
|
||||
mri@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue