Compare commits
4 Commits
0147956b5f
...
136-native
| Author | SHA1 | Date | |
|---|---|---|---|
| e7c3b50a79 | |||
| cba23eab14 | |||
| 02af1996ec | |||
| a2a597d3d8 |
24
README.md
@ -1,21 +1,29 @@
|
||||
# Drop Desktop Client
|
||||
# Drop App
|
||||
|
||||
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
|
||||
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.
|
||||
|
||||
## Internals
|
||||
## 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 Docs](https://docs.droposs.org/docs/guides/quickstart)
|
||||
|
||||
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
||||
## 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
|
||||
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
|
||||
|
||||
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
|
||||
Install dependencies with `yarn`
|
||||
|
||||
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
||||
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
||||
|
||||
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 tauri dev`
|
||||
|
||||
## Contributing
|
||||
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).
|
||||
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.
|
||||
@ -21,13 +21,6 @@ async function spawn(exec, opts) {
|
||||
});
|
||||
}
|
||||
|
||||
const expectedLibs = ["drop-base/package.json"];
|
||||
|
||||
for (const lib of expectedLibs) {
|
||||
const path = `./libs/${lib}`;
|
||||
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
|
||||
}
|
||||
|
||||
const views = fs.readdirSync(".").filter((view) => {
|
||||
const expectedPath = `./${view}/package.json`;
|
||||
return fs.existsSync(expectedPath);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator color="#2563eb" />
|
||||
<LoadingIndicator />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</NuxtLink>
|
||||
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||
<div class="flex flex-col mb-1">
|
||||
<MenuItem v-if="state.user.admin" v-slot="{ active }">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
:href="adminUrl"
|
||||
target="_blank"
|
||||
|
||||
@ -1,118 +1,55 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
>
|
||||
<MagnifyingGlassIcon
|
||||
class="h-5 w-5 text-zinc-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
<input type="text" v-model="searchQuery"
|
||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
placeholder="Search library..." />
|
||||
</div>
|
||||
<button
|
||||
@click="() => calculateGames(true, true)"
|
||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
||||
>
|
||||
<button @click="() => calculateGames(true)"
|
||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100">
|
||||
<ArrowPathIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||
<Disclosure
|
||||
as="div"
|
||||
v-for="(nav, navIndex) in filteredNavigation"
|
||||
:key="nav.id"
|
||||
class="first:pt-0 last:pb-0"
|
||||
v-slot="{ open }"
|
||||
:default-open="nav.deft"
|
||||
>
|
||||
<dt>
|
||||
<DisclosureButton
|
||||
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
|
||||
>
|
||||
<span class="text-sm font-semibold font-display">{{
|
||||
nav.name
|
||||
}}</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
|
||||
<NuxtLink
|
||||
v-for="item in nav.items"
|
||||
:key="nav.id"
|
||||
:class="[
|
||||
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||
currentNavigation == item.id
|
||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||
: item.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="item.route"
|
||||
>
|
||||
<div class="flex items-center w-full gap-x-2">
|
||||
<div
|
||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||
>
|
||||
<img
|
||||
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
|
||||
:src="icons[item.id]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
<p
|
||||
class="text-sm whitespace-nowrap font-display font-semibold"
|
||||
>
|
||||
{{ item.label }}
|
||||
</p>
|
||||
<p
|
||||
class="truncate text-[10px] font-bold uppercase font-display"
|
||||
:class="[
|
||||
gameStatusTextStyle[games[item.id].status.value.type],
|
||||
]"
|
||||
>
|
||||
{{ gameStatusText[games[item.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
|
||||
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||
navIndex === currentNavigation
|
||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||
: nav.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]" :href="nav.route">
|
||||
<div class="flex items-center w-full gap-x-3">
|
||||
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
|
||||
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||
:src="icons[nav.id]" alt="" />
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
||||
>
|
||||
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
|
||||
fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
@ -121,20 +58,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
MagnifyingGlassIcon,
|
||||
MinusSmallIcon,
|
||||
PlusSmallIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
GameStatusEnum,
|
||||
type Collection as Collection,
|
||||
type Game,
|
||||
type GameStatus,
|
||||
} from "~/types";
|
||||
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
|
||||
import { TransitionGroup } from "vue";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
@ -144,7 +70,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-700",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
@ -174,47 +100,26 @@ const games: {
|
||||
} = {};
|
||||
const icons: { [key: string]: string } = {};
|
||||
|
||||
const collections: Ref<Collection[]> = ref([]);
|
||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false, forceRefresh = false) {
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) {
|
||||
collections.value = [];
|
||||
rawGames.value = [];
|
||||
loading.value = true;
|
||||
}
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<Game[]>("fetch_library", {
|
||||
hardRefresh: forceRefresh,
|
||||
});
|
||||
const otherCollections = await invoke<Collection[]>("fetch_collections", {
|
||||
hardRefresh: forceRefresh,
|
||||
});
|
||||
const allGames = [
|
||||
...newGames,
|
||||
...otherCollections
|
||||
.map((e) => e.entries)
|
||||
.flat()
|
||||
.map((e) => e.game),
|
||||
].filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
for (const game of allGames) {
|
||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||
for (const game of newGames) {
|
||||
if (games[game.id]) continue;
|
||||
games[game.id] = await useGame(game.id);
|
||||
}
|
||||
for (const game of allGames) {
|
||||
for (const game of newGames) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
|
||||
const libraryCollection = {
|
||||
id: "library",
|
||||
name: "Library",
|
||||
isDefault: true,
|
||||
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
|
||||
} satisfies Collection;
|
||||
|
||||
loading.value = false;
|
||||
collections.value = [libraryCollection, ...otherCollections];
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
// Wait up to 300 ms for the library to load, otherwise
|
||||
@ -223,43 +128,36 @@ await new Promise<void>((r) => {
|
||||
let hasResolved = false;
|
||||
const resolveFunc = () => {
|
||||
if (!hasResolved) r();
|
||||
hasResolved = true;
|
||||
};
|
||||
hasResolved = true
|
||||
|
||||
}
|
||||
calculateGames(true).then(resolveFunc);
|
||||
setTimeout(resolveFunc, 300);
|
||||
});
|
||||
})
|
||||
|
||||
const navigation = computed(() =>
|
||||
collections.value.map((collection) => {
|
||||
const items = collection.entries.map(({ game }) => {
|
||||
const status = games[game.id].status;
|
||||
rawGames.value.map((game) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() => status.value.type != GameStatusEnum.Remote
|
||||
);
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type != GameStatusEnum.Remote
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
||||
return {
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
deft: collection.isDefault,
|
||||
items,
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
const route = useRoute();
|
||||
const currentNavigation = computed(() => {
|
||||
return route.path.slice("/library/".length);
|
||||
return navigation.value.findIndex((e) => e.route == route.path)
|
||||
});
|
||||
|
||||
const filteredNavigation = computed(() => {
|
||||
@ -267,18 +165,15 @@ const filteredNavigation = computed(() => {
|
||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return navigation.value
|
||||
.map((c) => ({
|
||||
...c,
|
||||
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
|
||||
}))
|
||||
.filter((e) => e.items.length > 0);
|
||||
.filter((nav) => nav.label.toLowerCase().includes(query))
|
||||
.map((e, i) => ({ ...e, index: i }));
|
||||
});
|
||||
|
||||
listen("update_library", async (event) => {
|
||||
console.log("Updating library");
|
||||
let oldNavigation = currentNavigation.value;
|
||||
let oldNavigation = navigation.value[currentNavigation.value];
|
||||
await calculateGames();
|
||||
if (oldNavigation !== currentNavigation.value) {
|
||||
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
||||
router.push("/library");
|
||||
}
|
||||
});
|
||||
|
||||
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const loading = useLoadingIndicator();
|
||||
|
||||
watch(loading.isLoading, console.log);
|
||||
</script>
|
||||
@ -32,5 +32,3 @@ listen("update_stats", (event) => {
|
||||
const stats = useStatsState();
|
||||
stats.value = event.payload as StatsState;
|
||||
});
|
||||
|
||||
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
|
||||
@ -4,18 +4,18 @@
|
||||
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
||||
>
|
||||
<div
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
|
||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
|
||||
>
|
||||
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
|
||||
<span v-if="stats.time > 0" class="text-xs text-zinc-400"
|
||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||
<span v-if="stats.time > 0" class="text-sm"
|
||||
>{{ formatTime(stats.time) }} left</span
|
||||
>
|
||||
</div>
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
|
||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||
<div
|
||||
v-for="bar in speedHistory"
|
||||
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||
class="w-[3px] bg-blue-600 rounded-t-full"
|
||||
class="w-[8px] bg-blue-600/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -62,9 +62,9 @@
|
||||
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
||||
><span class="text-zinc-300">{{
|
||||
formatKilobytes(element.current / 1000)
|
||||
}}B</span>
|
||||
}}</span>
|
||||
/
|
||||
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
|
||||
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
|
||||
><ServerIcon class="size-5"
|
||||
/></span>
|
||||
</div>
|
||||
@ -91,7 +91,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||
|
||||
// const actionNames = {
|
||||
// [GameStatusEnum.Downloading]: "downloading",
|
||||
@ -105,12 +105,12 @@ window.addEventListener("resize", (event) => {
|
||||
|
||||
const queue = useQueueState();
|
||||
const stats = useStatsState();
|
||||
const speedHistory = useDownloadHistory();
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 4);
|
||||
const speedHistory = useState<Array<number>>(() => []);
|
||||
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||
const speedMax = computed(
|
||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
|
||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
|
||||
);
|
||||
const previousGameId = useState<string | undefined>('previous_game');
|
||||
const previousGameId = ref<string | undefined>();
|
||||
|
||||
const games: Ref<{
|
||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||
@ -122,15 +122,14 @@ function resetHistoryGraph() {
|
||||
}
|
||||
function checkReset(v: QueueState) {
|
||||
const currentGame = v.queue.at(0)?.meta.id;
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
|
||||
// If we're finished
|
||||
if (!currentGame && previousGameId.value) {
|
||||
previousGameId.value = undefined;
|
||||
resetHistoryGraph();
|
||||
return;
|
||||
}
|
||||
// If we don't have a game
|
||||
if (!currentGame) return;
|
||||
// If we started a new download
|
||||
if (currentGame && !previousGameId.value) {
|
||||
previousGameId.value = currentGame;
|
||||
@ -150,10 +149,9 @@ watch(queue, (v) => {
|
||||
});
|
||||
|
||||
watch(stats, (v) => {
|
||||
if(v.speed == 0) return;
|
||||
const newLength = speedHistory.value.push(v.speed);
|
||||
if (newLength > speedHistoryMax.value) {
|
||||
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
|
||||
speedHistory.value.splice(0, 1);
|
||||
}
|
||||
checkReset(queue.value);
|
||||
});
|
||||
@ -185,7 +183,7 @@ async function cancelGame(meta: DownloadableMetadata) {
|
||||
}
|
||||
|
||||
function formatKilobytes(bytes: number): string {
|
||||
const units = ["K", "M", "G", "T", "P"];
|
||||
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
const scalar = 1000;
|
||||
|
||||
@ -37,13 +37,6 @@ export type Game = {
|
||||
mImageCarouselObjectIds: string[];
|
||||
};
|
||||
|
||||
export type Collection = {
|
||||
id: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
entries: Array<{ gameId: string; game: Game }>;
|
||||
};
|
||||
|
||||
export type GameVersion = {
|
||||
launchCommandTemplate: string;
|
||||
};
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"tauri": "^0.15.0"
|
||||
"pino-pretty": "^13.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.7.1"
|
||||
|
||||
2343
src-tauri/Cargo.lock
generated
@ -1,14 +1,118 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"client",
|
||||
"database",
|
||||
"src-tauri",
|
||||
"process",
|
||||
"remote",
|
||||
"utils",
|
||||
"cloud_saves",
|
||||
"download_manager",
|
||||
"games",
|
||||
]
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.3"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
|
||||
resolver = "3"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "drop_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde_json = "1"
|
||||
rayon = "1.10.0"
|
||||
webbrowser = "1.0.2"
|
||||
url = "2.5.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
log = "0.4.22"
|
||||
hex = "0.4.3"
|
||||
tauri-plugin-dialog = "2"
|
||||
http = "1.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
md5 = "0.7.0"
|
||||
chrono = "0.4.38"
|
||||
tauri-plugin-os = "2"
|
||||
boxcar = "0.2.7"
|
||||
umu-wrapper-lib = "0.1.0"
|
||||
tauri-plugin-autostart = "2.0.0"
|
||||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
slice-deque = "0.3.0"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
atomic-instant-full = "0.1.0"
|
||||
cacache = "13.1.0"
|
||||
http-serde = "2.1.1"
|
||||
reqwest-middleware = "0.4.0"
|
||||
reqwest-middleware-cache = "0.1.1"
|
||||
deranged = "=0.4.0"
|
||||
droplet-rs = "0.7.3"
|
||||
gethostname = "1.0.1"
|
||||
zstd = "0.13.3"
|
||||
tar = "0.4.44"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
tempfile = "3.19.1"
|
||||
schemars = "0.8.22"
|
||||
sha1 = "0.10.6"
|
||||
dirs = "6.0.0"
|
||||
whoami = "1.6.0"
|
||||
filetime = "0.2.25"
|
||||
walkdir = "2.5.0"
|
||||
known-folders = "1.2.0"
|
||||
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
bitcode = "0.6.6"
|
||||
reqwest-websocket = "0.5.0"
|
||||
futures-lite = "2.6.0"
|
||||
page_size = "0.6.0"
|
||||
sysinfo = "0.36.1"
|
||||
humansize = "2.1.3"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
version = "0.1.5"
|
||||
features = ["curly"]
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.7.0"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.40.0"
|
||||
features = ["rt", "tokio-macros", "signal"]
|
||||
|
||||
[dependencies.log4rs]
|
||||
version = "1.3.0"
|
||||
features = ["console_appender", "file_appender"]
|
||||
|
||||
[dependencies.rustix]
|
||||
version = "0.38.37"
|
||||
features = ["fs"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.10.0"
|
||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||
|
||||
[dependencies.rustbreak]
|
||||
version = "2"
|
||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.22"
|
||||
default-features = false
|
||||
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive", "rc"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
|
||||
4862
src-tauri/client/Cargo.lock
generated
@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitcode = "0.6.7"
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
log = "0.4.28"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tauri = "2.8.5"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
@ -1,12 +0,0 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
||||
pub enum AppStatus {
|
||||
NotConfigured,
|
||||
Offline,
|
||||
ServerError,
|
||||
SignedOut,
|
||||
SignedIn,
|
||||
SignedInNeedsReauth,
|
||||
ServerUnavailable,
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
pub mod autostart;
|
||||
pub mod user;
|
||||
pub mod app_status;
|
||||
@ -1,17 +0,0 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
id: String,
|
||||
username: String,
|
||||
admin: bool,
|
||||
display_name: String,
|
||||
profile_picture_object_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompatInfo {
|
||||
umu_installed: bool,
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "cloud_saves"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
dirs = "6.0.0"
|
||||
log = "0.4.28"
|
||||
regex = "1.11.3"
|
||||
rustix = "1.1.2"
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.145"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.23.0"
|
||||
uuid = "1.18.1"
|
||||
whoami = "1.6.1"
|
||||
zstd = "0.13.3"
|
||||
@ -1,5 +0,0 @@
|
||||
pub enum BackupError {
|
||||
InvalidSystem,
|
||||
ParseError,
|
||||
NotFound
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
pub mod conditions;
|
||||
pub mod metadata;
|
||||
pub mod resolver;
|
||||
pub mod placeholder;
|
||||
pub mod normalise;
|
||||
pub mod path;
|
||||
pub mod backup_manager;
|
||||
pub mod error;
|
||||
@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "database"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.42"
|
||||
dirs = "6.0.0"
|
||||
log = "0.4.28"
|
||||
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||
rustbreak = "2.0.0"
|
||||
serde = "1.0.228"
|
||||
serde_with = "3.15.0"
|
||||
url = "2.5.7"
|
||||
whoami = "1.6.1"
|
||||
@ -1,47 +0,0 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use rustbreak::{DeSerError, DeSerializer};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::interface::{DatabaseImpls, DatabaseInterface};
|
||||
|
||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static DATA_ROOT_PREFIX: &str = "drop";
|
||||
#[cfg(debug_assertions)]
|
||||
static DATA_ROOT_PREFIX: &str = "drop-debug";
|
||||
|
||||
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
|
||||
Arc::new(
|
||||
dirs::data_dir()
|
||||
.expect("Failed to get data dir")
|
||||
.join(DATA_ROOT_PREFIX),
|
||||
)
|
||||
});
|
||||
|
||||
// Custom JSON serializer to support everything we need
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DropDatabaseSerializer;
|
||||
|
||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||
for DropDatabaseSerializer
|
||||
{
|
||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||
native_model::encode(val)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
||||
}
|
||||
|
||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||
let mut buf = Vec::new();
|
||||
s.read_to_end(&mut buf)
|
||||
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||
let (val, _version) = native_model::decode(buf)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
#![feature(nonpoison_rwlock)]
|
||||
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod models;
|
||||
pub mod platform;
|
||||
pub mod interface;
|
||||
|
||||
pub use models::data::{
|
||||
ApplicationTransientStatus,
|
||||
Database,
|
||||
DatabaseApplications,
|
||||
DatabaseAuth,
|
||||
DownloadType,
|
||||
DownloadableMetadata,
|
||||
GameDownloadStatus,
|
||||
GameVersion,
|
||||
Settings
|
||||
};
|
||||
pub use db::DB;
|
||||
pub use interface::{borrow_db_checked, borrow_db_mut_checked};
|
||||
@ -1,47 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Linux,
|
||||
MacOs,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const HOST: Platform = Self::Windows;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const HOST: Platform = Self::MacOs;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const HOST: Platform = Self::Linux;
|
||||
|
||||
pub fn is_case_sensitive(&self) -> bool {
|
||||
match self {
|
||||
Self::Windows | Self::MacOs => false,
|
||||
Self::Linux => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Platform {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().trim() {
|
||||
"windows" => Self::Windows,
|
||||
"linux" => Self::Linux,
|
||||
"mac" | "macos" => Self::MacOs,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<whoami::Platform> for Platform {
|
||||
fn from(value: whoami::Platform) -> Self {
|
||||
match value {
|
||||
whoami::Platform::Windows => Platform::Windows,
|
||||
whoami::Platform::Linux => Platform::Linux,
|
||||
whoami::Platform::MacOS => Platform::MacOs,
|
||||
platform => unimplemented!("Playform {} is not supported", platform),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "download_manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
atomic-instant-full = "0.1.0"
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
humansize = "2.1.3"
|
||||
log = "0.4.28"
|
||||
parking_lot = "0.12.5"
|
||||
remote = { version = "0.1.0", path = "../remote" }
|
||||
serde = "1.0.228"
|
||||
serde_with = "3.15.0"
|
||||
tauri = "2.8.5"
|
||||
throttle_my_fn = "0.2.6"
|
||||
utils = { version = "0.1.0", path = "../utils" }
|
||||
@ -1,24 +0,0 @@
|
||||
use database::DownloadableMetadata;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::download_manager_frontend::DownloadStatus;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct QueueUpdateEventQueueData {
|
||||
pub meta: DownloadableMetadata,
|
||||
pub status: DownloadStatus,
|
||||
pub progress: f64,
|
||||
pub current: usize,
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct QueueUpdateEvent {
|
||||
pub queue: Vec<QueueUpdateEventQueueData>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct StatsUpdateEvent {
|
||||
pub speed: usize,
|
||||
pub time: usize,
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "games"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
atomic-instant-full = "0.1.0"
|
||||
bitcode = "0.6.7"
|
||||
boxcar = "0.2.14"
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
download_manager = { version = "0.1.0", path = "../download_manager" }
|
||||
hex = "0.4.3"
|
||||
log = "0.4.28"
|
||||
md5 = "0.8.0"
|
||||
rayon = "1.11.0"
|
||||
remote = { version = "0.1.0", path = "../remote" }
|
||||
reqwest = "0.12.23"
|
||||
rustix = "1.1.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_with = "3.15.0"
|
||||
sysinfo = "0.37.2"
|
||||
tauri = "2.8.5"
|
||||
throttle_my_fn = "0.2.6"
|
||||
utils = { version = "0.1.0", path = "../utils" }
|
||||
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||
serde_json = "1.0.145"
|
||||
@ -1,21 +0,0 @@
|
||||
use std::fmt::{Display};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum LibraryError {
|
||||
MetaNotFound(String),
|
||||
VersionNotFound(String),
|
||||
}
|
||||
impl Display for LibraryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
LibraryError::MetaNotFound(id) => {
|
||||
format!("Could not locate any installed version of game ID {id} in the database")
|
||||
}
|
||||
LibraryError::VersionNotFound(game_id) => {
|
||||
format!("Could not locate any installed version for game id {game_id} in the database")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,313 +0,0 @@
|
||||
use std::fs::remove_dir_all;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
use bitcode::{Decode, Encode};
|
||||
use database::{borrow_db_checked, borrow_db_mut_checked, ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion};
|
||||
use log::{debug, error, warn};
|
||||
use remote::{auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url, utils::DROP_CLIENT_SYNC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use utils::app_emit;
|
||||
|
||||
use crate::{downloads::error::LibraryError, state::{GameStatusManager, GameStatusWithTransient}};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FetchGameStruct {
|
||||
game: Game,
|
||||
status: GameStatusWithTransient,
|
||||
version: Option<GameVersion>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Game {
|
||||
id: String,
|
||||
m_name: String,
|
||||
m_short_description: String,
|
||||
m_description: String,
|
||||
// mDevelopers
|
||||
// mPublishers
|
||||
m_icon_object_id: String,
|
||||
m_banner_object_id: String,
|
||||
m_cover_object_id: String,
|
||||
m_image_library_object_ids: Vec<String>,
|
||||
m_image_carousel_object_ids: Vec<String>,
|
||||
}
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct GameUpdateEvent {
|
||||
pub game_id: String,
|
||||
pub status: (
|
||||
Option<GameDownloadStatus>,
|
||||
Option<ApplicationTransientStatus>,
|
||||
),
|
||||
pub version: Option<GameVersion>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by:
|
||||
* - on_cancel, when cancelled, for obvious reasons
|
||||
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
|
||||
* - when scanning, to import the game
|
||||
*/
|
||||
pub fn set_partially_installed(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
) {
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
|
||||
}
|
||||
|
||||
pub fn set_partially_installed_db(
|
||||
db_lock: &mut Database,
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
) {
|
||||
db_lock.applications.transient_statuses.remove(meta);
|
||||
db_lock.applications.game_statuses.insert(
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.as_ref().unwrap().clone(),
|
||||
install_dir,
|
||||
},
|
||||
);
|
||||
db_lock
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
if let Some(app_handle) = app_handle {
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, db_lock),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
debug!("triggered uninstall for agent");
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
|
||||
let previous_state = if let Some(state) = previous_state {
|
||||
state
|
||||
} else {
|
||||
warn!("uninstall job doesn't have previous state, failing silently");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((_, 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)),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
spawn(move || {
|
||||
if let Err(e) = remove_dir_all(install_dir) {
|
||||
error!("{e}");
|
||||
} else {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_emit!(app_handle, "update_library", ());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("invalid previous state for uninstall, failing silently.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
borrow_db_checked()
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(game_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = generate_url(
|
||||
&["/api/v1/client/game/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
)?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()?;
|
||||
|
||||
let game_version: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.entry(meta.id.clone())
|
||||
.or_default()
|
||||
.insert(meta.version.clone().unwrap(), game_version.clone());
|
||||
handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
|
||||
let status = if game_version.setup_command.is_empty() {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
}
|
||||
} else {
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
}
|
||||
};
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
app_emit!(
|
||||
app_handle,
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(status), None),
|
||||
version: Some(game_version),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_game_update(
|
||||
app_handle: &AppHandle,
|
||||
game_id: &String,
|
||||
version: Option<GameVersion>,
|
||||
status: GameStatusWithTransient,
|
||||
) {
|
||||
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
||||
&status.0
|
||||
&& version.is_none()
|
||||
{
|
||||
panic!("pushed game for installed game that doesn't have version information");
|
||||
}
|
||||
|
||||
app_emit!(
|
||||
app_handle,
|
||||
&format!("update_game/{game_id}"),
|
||||
GameUpdateEvent {
|
||||
game_id: game_id.clone(),
|
||||
status,
|
||||
version,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
launch_string: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_game_configuration(
|
||||
game_id: String,
|
||||
options: FrontendGameOptions,
|
||||
) -> Result<(), LibraryError> {
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let installed_version = handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||
|
||||
let id = installed_version.id.clone();
|
||||
let version = installed_version.version.clone().ok_or(LibraryError::VersionNotFound(id.clone()))?;
|
||||
|
||||
let mut existing_configuration = handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&id)
|
||||
.unwrap()
|
||||
.get(&version)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// Add more options in here
|
||||
existing_configuration.launch_command_template = options.launch_string;
|
||||
|
||||
// Add no more options past here
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.insert(version.to_string(), existing_configuration);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 911 B After Width: | Height: | Size: 911 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 803 B |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 515 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "process"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.42"
|
||||
client = { version = "0.1.0", path = "../client" }
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
dynfmt = "0.1.5"
|
||||
log = "0.4.28"
|
||||
serde = "1.0.228"
|
||||
serde_with = "3.15.0"
|
||||
shared_child = "1.1.1"
|
||||
tauri = "2.8.5"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
utils = { version = "0.1.0", path = "../utils" }
|
||||
@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "remote"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitcode = "0.6.7"
|
||||
chrono = "0.4.42"
|
||||
client = { version = "0.1.0", path = "../client" }
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
droplet-rs = "0.7.3"
|
||||
gethostname = "1.0.2"
|
||||
hex = "0.4.3"
|
||||
http = "1.3.1"
|
||||
log = "0.4.28"
|
||||
md5 = "0.8.0"
|
||||
reqwest = "0.12.23"
|
||||
reqwest-websocket = "0.5.1"
|
||||
serde = "1.0.228"
|
||||
serde_with = "3.15.0"
|
||||
tauri = "2.8.5"
|
||||
url = "2.5.7"
|
||||
utils = { version = "0.1.0", path = "../utils" }
|
||||
@ -1,136 +0,0 @@
|
||||
use std::{collections::HashMap, env};
|
||||
|
||||
use chrono::Utc;
|
||||
use client::{app_status::AppStatus, user::User};
|
||||
use database::interface::borrow_db_checked;
|
||||
use droplet_rs::ssl::sign_nonce;
|
||||
use gethostname::gethostname;
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{error::{DropServerError, RemoteAccessError}, requests::make_authenticated_get, utils::DROP_CLIENT_SYNC};
|
||||
|
||||
use super::{
|
||||
cache::{cache_object, get_cached_object},
|
||||
requests::generate_url,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CapabilityConfiguration {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InitiateRequestBody {
|
||||
name: String,
|
||||
platform: String,
|
||||
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeRequestBody {
|
||||
client_id: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeResponse {
|
||||
private: String,
|
||||
certificate: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = borrow_db_checked();
|
||||
db.auth.clone().expect("Authorisation not initialised")
|
||||
};
|
||||
|
||||
let nonce = Utc::now().timestamp_millis().to_string();
|
||||
|
||||
let signature =
|
||||
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
|
||||
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
}
|
||||
|
||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json().await?;
|
||||
warn!("{err:?}");
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<User>()
|
||||
.await
|
||||
.map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = borrow_db_checked();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
let hostname = gethostname();
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||
let body = InitiateRequestBody {
|
||||
name: format!("{} (Desktop)", hostname.display()),
|
||||
platform: env::consts::OS.to_string(),
|
||||
capabilities: HashMap::from([
|
||||
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||
]),
|
||||
mode,
|
||||
};
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let response = response.text()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
||||
let auth = {
|
||||
let data = borrow_db_checked();
|
||||
data.auth.clone()
|
||||
};
|
||||
|
||||
if auth.is_some() {
|
||||
let user_result = match fetch_user().await {
|
||||
Ok(data) => data,
|
||||
Err(RemoteAccessError::FetchError(_)) => {
|
||||
let user = get_cached_object::<User>("user").ok();
|
||||
return (AppStatus::Offline, user);
|
||||
}
|
||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||
};
|
||||
if let Err(e) = cache_object("user", &user_result) {
|
||||
warn!("Could not cache user object with error {e}");
|
||||
}
|
||||
return (AppStatus::SignedIn, Some(user_result));
|
||||
}
|
||||
|
||||
(AppStatus::SignedOut, None)
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
use database::{interface::DatabaseImpls, DB};
|
||||
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
|
||||
use log::{debug, warn};
|
||||
use tauri::UriSchemeResponder;
|
||||
|
||||
use crate::{error::CacheError, utils::DROP_CLIENT_ASYNC};
|
||||
|
||||
use super::{
|
||||
auth::generate_authorization_header,
|
||||
cache::{ObjectCache, cache_object, get_cached_object},
|
||||
};
|
||||
|
||||
pub async fn fetch_object_wrapper(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
match fetch_object(request).await {
|
||||
Ok(r) => responder.respond(r),
|
||||
Err(e) => {
|
||||
warn!("Cache error: {e}");
|
||||
responder.respond(Response::builder().status(500).body(Vec::new()).expect("Failed to build error response"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn fetch_object(request: http::Request<Vec<u8>>) -> Result<Response<Vec<u8>>, CacheError>
|
||||
{
|
||||
// Drop leading /
|
||||
let object_id = &request.uri().path()[1..];
|
||||
|
||||
let cache_result = get_cached_object::<ObjectCache>(object_id);
|
||||
if let Ok(cache_result) = &cache_result
|
||||
&& !cache_result.has_expired()
|
||||
{
|
||||
return cache_result.try_into();
|
||||
}
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
|
||||
let response = client.get(url).header("Authorization", header).send().await;
|
||||
|
||||
match response {
|
||||
Ok(r) => {
|
||||
let resp_builder = ResponseBuilder::new().header(
|
||||
CONTENT_TYPE,
|
||||
r.headers()
|
||||
.get("Content-Type")
|
||||
.expect("Failed get Content-Type header"),
|
||||
);
|
||||
let data = match r.bytes().await {
|
||||
Ok(data) => Vec::from(data),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Could not get data from cache object {object_id} with error {e}",
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let resp = resp_builder.body(data).expect("Failed to build object cache response body");
|
||||
if cache_result.map_or(true, |x| x.has_expired()) {
|
||||
cache_object::<ObjectCache>(object_id, &resp.clone().try_into()?)
|
||||
.expect("Failed to create cached object");
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Object fetch failed with error {e}. Attempting to download from cache");
|
||||
match cache_result {
|
||||
Ok(cache_result) => cache_result.try_into(),
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
Err(CacheError::Remote(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use database::borrow_db_checked;
|
||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
||||
use log::{error, warn};
|
||||
use tauri::UriSchemeResponder;
|
||||
use utils::webbrowser_open::webbrowser_open;
|
||||
|
||||
use crate::utils::DROP_CLIENT_SYNC;
|
||||
|
||||
pub async fn handle_server_proto_offline_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
responder.respond(match handle_server_proto_offline(request).await {
|
||||
Ok(res) => res,
|
||||
Err(_) => unreachable!()
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode>{
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Vec::new())
|
||||
.expect("Failed to build error response for proto offline"))
|
||||
|
||||
}
|
||||
|
||||
pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
match handle_server_proto(request).await {
|
||||
Ok(r) => responder.respond(r),
|
||||
Err(e) => {
|
||||
warn!("Cache error: {e}");
|
||||
responder.respond(Response::builder().status(e).body(Vec::new()).expect("Failed to build error response"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let auth = match db_handle.auth.as_ref() {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
error!("Could not find auth in database");
|
||||
return Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
};
|
||||
let web_token = match &auth.web_token {
|
||||
Some(token) => token,
|
||||
None => return Err(StatusCode::UNAUTHORIZED),
|
||||
};
|
||||
let remote_uri = db_handle.base_url.parse::<Uri>().expect("Failed to parse base url");
|
||||
|
||||
let path = request.uri().path();
|
||||
|
||||
let mut new_uri = request.uri().clone().into_parts();
|
||||
new_uri.path_and_query =
|
||||
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).expect("Failed to parse request path in proto"));
|
||||
new_uri.authority = remote_uri.authority().cloned();
|
||||
new_uri.scheme = remote_uri.scheme().cloned();
|
||||
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
|
||||
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
|
||||
|
||||
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
||||
|
||||
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
||||
webbrowser_open(new_uri.to_string());
|
||||
return Ok(Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = match client
|
||||
.request(request.method().clone(), new_uri.to_string())
|
||||
.header("Authorization", format!("Bearer {web_token}"))
|
||||
.headers(request.headers().clone())
|
||||
.send() {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
warn!("Could not send response. Got {e} when sending");
|
||||
return Err(e.status().unwrap_or(StatusCode::BAD_REQUEST))
|
||||
},
|
||||
};
|
||||
|
||||
let response_status = response.status();
|
||||
let response_body = match response.bytes() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||
};
|
||||
|
||||
let http_response = Response::builder()
|
||||
.status(response_status)
|
||||
.body(response_body.to_vec())
|
||||
.expect("Failed to build server proto response");
|
||||
|
||||
Ok(http_response)
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use database::db::DATA_ROOT_DIR;
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::Certificate;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
|
||||
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
|
||||
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
|
||||
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
||||
|
||||
fn fetch_certificates() -> Vec<Certificate> {
|
||||
let certificate_dir = DATA_ROOT_DIR.join("certificates");
|
||||
|
||||
let mut certs = Vec::new();
|
||||
match fs::read_dir(certificate_dir) {
|
||||
Ok(c) => {
|
||||
for entry in c {
|
||||
match entry {
|
||||
Ok(c) => {
|
||||
let mut buf = Vec::new();
|
||||
match File::open(c.path()) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to open file at {} with error {}",
|
||||
c.path().display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
.read_to_end(&mut buf)
|
||||
.unwrap_or_else(|e| panic!(
|
||||
"Failed to read to end of certificate file {} with error {}",
|
||||
c.path().display(),
|
||||
e
|
||||
));
|
||||
|
||||
match Certificate::from_pem_bundle(&buf) {
|
||||
Ok(certificates) => {
|
||||
for cert in certificates {
|
||||
certs.push(cert);
|
||||
}
|
||||
info!(
|
||||
"added {} certificate(s) from {}",
|
||||
certs.len(),
|
||||
c.file_name().display()
|
||||
);
|
||||
}
|
||||
Err(e) => warn!(
|
||||
"Invalid certificate file {} with error {}",
|
||||
c.path().display(),
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("not loading certificates due to error: {e}");
|
||||
}
|
||||
};
|
||||
certs
|
||||
}
|
||||
|
||||
pub fn get_client_sync() -> reqwest::blocking::Client {
|
||||
let mut client = reqwest::blocking::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client.use_rustls_tls().build().expect("Failed to build synchronous client")
|
||||
}
|
||||
pub fn get_client_async() -> reqwest::Client {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client.use_rustls_tls().build().expect("Failed to build asynchronous client")
|
||||
}
|
||||
pub fn get_client_ws() -> reqwest::Client {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client
|
||||
.use_rustls_tls()
|
||||
.http1_only()
|
||||
.build()
|
||||
.expect("Failed to build websocket client")
|
||||
}
|
||||
7741
src-tauri/src-tauri/Cargo.lock
generated
@ -1,137 +0,0 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.3"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "drop_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde_json = "1"
|
||||
rayon = "1.10.0"
|
||||
webbrowser = "1.0.2"
|
||||
url = "2.5.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
log = "0.4.22"
|
||||
hex = "0.4.3"
|
||||
tauri-plugin-dialog = "2"
|
||||
http = "1.1.0"
|
||||
urlencoding = "2.1.3"
|
||||
md5 = "0.7.0"
|
||||
chrono = "0.4.38"
|
||||
tauri-plugin-os = "2"
|
||||
boxcar = "0.2.7"
|
||||
umu-wrapper-lib = "0.1.0"
|
||||
tauri-plugin-autostart = "2.0.0"
|
||||
shared_child = "1.0.1"
|
||||
serde_with = "3.12.0"
|
||||
slice-deque = "0.3.0"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
atomic-instant-full = "0.1.0"
|
||||
cacache = "13.1.0"
|
||||
http-serde = "2.1.1"
|
||||
reqwest-middleware = "0.4.0"
|
||||
reqwest-middleware-cache = "0.1.1"
|
||||
deranged = "=0.4.0"
|
||||
droplet-rs = "0.7.3"
|
||||
gethostname = "1.0.1"
|
||||
zstd = "0.13.3"
|
||||
tar = "0.4.44"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
tempfile = "3.19.1"
|
||||
schemars = "0.8.22"
|
||||
sha1 = "0.10.6"
|
||||
dirs = "6.0.0"
|
||||
whoami = "1.6.0"
|
||||
filetime = "0.2.25"
|
||||
walkdir = "2.5.0"
|
||||
known-folders = "1.2.0"
|
||||
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
bitcode = "0.6.6"
|
||||
reqwest-websocket = "0.5.0"
|
||||
futures-lite = "2.6.0"
|
||||
page_size = "0.6.0"
|
||||
sysinfo = "0.36.1"
|
||||
humansize = "2.1.3"
|
||||
tokio-util = { version = "0.7.16", features = ["io"] }
|
||||
futures-core = "0.3.31"
|
||||
bytes = "1.10.1"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
|
||||
# Workspaces
|
||||
client = { path = "../client" }
|
||||
database = { path = "../database" }
|
||||
process = { path = "../process" }
|
||||
remote = { path = "../remote" }
|
||||
utils = { path = "../utils" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
version = "0.1.5"
|
||||
features = ["curly"]
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.7.0"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.40.0"
|
||||
features = ["rt", "tokio-macros", "signal"]
|
||||
|
||||
[dependencies.log4rs]
|
||||
version = "1.3.0"
|
||||
features = ["console_appender", "file_appender"]
|
||||
|
||||
[dependencies.rustix]
|
||||
version = "0.38.37"
|
||||
features = ["fs"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.10.0"
|
||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||
|
||||
[dependencies.rustbreak]
|
||||
version = "2"
|
||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.22"
|
||||
default-features = false
|
||||
features = [
|
||||
"json",
|
||||
"http2",
|
||||
"blocking",
|
||||
"rustls-tls",
|
||||
"native-tls-alpn",
|
||||
"rustls-tls-native-roots",
|
||||
"stream",
|
||||
]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive", "rc"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,40 +0,0 @@
|
||||
use crate::{lock, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_state(
|
||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||
) -> Result<String, String> {
|
||||
let guard = lock!(state);
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
cleanup_and_exit(&app, &state);
|
||||
}
|
||||
|
||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
debug!("cleaning up and exiting application");
|
||||
let download_manager = lock!(state).download_manager.clone();
|
||||
match download_manager.ensure_terminated() {
|
||||
Ok(res) => match res {
|
||||
Ok(()) => debug!("download manager terminated correctly"),
|
||||
Err(()) => error!("download manager failed to terminate correctly"),
|
||||
},
|
||||
Err(e) => panic!("{e:?}"),
|
||||
}
|
||||
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
toggle_autostart_logic(app, enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
get_autostart_enabled_logic(app)
|
||||
}
|
||||
@ -1,318 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::GameVersion,
|
||||
},
|
||||
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
|
||||
games::library::{
|
||||
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
||||
uninstall_game_logic,
|
||||
},
|
||||
offline,
|
||||
};
|
||||
|
||||
use super::{
|
||||
library::{
|
||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic,
|
||||
fetch_library_logic,
|
||||
},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_library(
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
hard_refresh: Option<bool>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
fetch_library_logic,
|
||||
fetch_library_logic_offline,
|
||||
state,
|
||||
hard_refresh
|
||||
).await
|
||||
}
|
||||
|
||||
|
||||
pub async fn fetch_library_logic(
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
hard_fresh: Option<bool>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let do_hard_refresh = hard_fresh.unwrap_or(false);
|
||||
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await.unwrap_or(DropServerError {
|
||||
status_code: 500,
|
||||
status_message: "Invalid response from server.".to_owned(),
|
||||
});
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let mut games: Vec<Game> = response.json().await?;
|
||||
|
||||
let mut handle = lock!(state);
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
for game in &games {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
if !db_handle.applications.game_statuses.contains_key(&game.id) {
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(game.id.clone(), GameDownloadStatus::Remote {});
|
||||
}
|
||||
}
|
||||
|
||||
// Add games that are installed but no longer in library
|
||||
for meta in db_handle.applications.installed_game_version.values() {
|
||||
if games.iter().any(|e| e.id == meta.id) {
|
||||
continue;
|
||||
}
|
||||
// We should always have a cache of the object
|
||||
// Pass db_handle because otherwise we get a gridlock
|
||||
let game = match get_cached_object_db::<Game>(&meta.id.clone(), &db_handle) {
|
||||
Ok(game) => game,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{} is installed, but encountered error fetching its error: {}.",
|
||||
meta.id, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
games.push(game);
|
||||
}
|
||||
|
||||
drop(handle);
|
||||
drop(db_handle);
|
||||
cache_object("library", &games)?;
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub async fn fetch_library_logic_offline(
|
||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
_hard_refresh: Option<bool>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||
|
||||
let db_handle = borrow_db_checked();
|
||||
|
||||
games.retain(|game| {
|
||||
matches!(
|
||||
&db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game.id)
|
||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
||||
)
|
||||
});
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub async fn fetch_game_logic(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let version = {
|
||||
let state_handle = lock!(state);
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
|
||||
let game = state_handle.games.get(&id);
|
||||
if let Some(game) = game {
|
||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
status,
|
||||
version,
|
||||
};
|
||||
|
||||
cache_object_db(&id, game, &db_lock)?;
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
version
|
||||
};
|
||||
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() == 404 {
|
||||
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
|
||||
if let Ok(fetch_data) = offline_fetch {
|
||||
return Ok(fetch_data);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::GameNotFound(id));
|
||||
}
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await?;
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let game: Game = response.json().await?;
|
||||
|
||||
let mut state_handle = lock!(state);
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(id.clone())
|
||||
.or_insert(GameDownloadStatus::Remote {});
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
status,
|
||||
version,
|
||||
};
|
||||
|
||||
cache_object(&id, &game)?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn fetch_game_version_options_logic(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await?;
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let data: Vec<GameVersion> = response.json().await?;
|
||||
|
||||
let state_lock = lock!(state);
|
||||
let process_manager_lock = lock!(state_lock.process_manager);
|
||||
let data: Vec<GameVersion> = data
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform, &state_lock))
|
||||
.collect();
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
|
||||
pub async fn fetch_game_logic_offline(
|
||||
id: String,
|
||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => db_handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
let game = get_cached_object::<Game>(&id)?;
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
Ok(FetchGameStruct {
|
||||
game,
|
||||
status,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
fetch_game_logic,
|
||||
fetch_game_logic_offline,
|
||||
game_id,
|
||||
state
|
||||
).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||
let db_handle = borrow_db_checked();
|
||||
GameStatusManager::fetch_state(&id, &db_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||
let meta = match get_current_meta(&game_id) {
|
||||
Some(data) => data,
|
||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||
};
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_game_version_options(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
fetch_game_version_options_logic(game_id, state).await
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
use database::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
@ -64,3 +64,12 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
toggle_autostart_logic(app, enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
get_autostart_enabled_logic(app)
|
||||
}
|
||||
23
src-tauri/src/client/cleanup.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use log::{debug, error};
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
cleanup_and_exit(&app, &state);
|
||||
}
|
||||
|
||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
debug!("cleaning up and exiting application");
|
||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||
match download_manager.ensure_terminated() {
|
||||
Ok(res) => match res {
|
||||
Ok(()) => debug!("download manager terminated correctly"),
|
||||
Err(()) => error!("download manager failed to terminate correctly"),
|
||||
},
|
||||
Err(e) => panic!("{e:?}"),
|
||||
}
|
||||
|
||||
app.exit(0);
|
||||
}
|
||||
11
src-tauri/src/client/commands.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_state(
|
||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||
) -> Result<String, String> {
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
}
|
||||
3
src-tauri/src/client/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod autostart;
|
||||
pub mod cleanup;
|
||||
pub mod commands;
|
||||
@ -1,11 +1,8 @@
|
||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use database::platform::Platform;
|
||||
use database::{db::DATA_ROOT_DIR, GameVersion};
|
||||
use log::warn;
|
||||
|
||||
use crate::error::BackupError;
|
||||
use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
|
||||
|
||||
use super::path::CommonPath;
|
||||
|
||||
@ -48,7 +45,7 @@ impl BackupManager<'_> {
|
||||
}
|
||||
|
||||
pub trait BackupHandler: Send + Sync {
|
||||
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.join("games")) }
|
||||
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
|
||||
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
|
||||
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
|
||||
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
|
||||