Compare commits
127 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9c4f7aa75 | ||
|
|
316a3742eb | ||
|
|
b9df197534 | ||
|
|
5c479cb283 | ||
|
|
4c59c5d6c1 | ||
|
|
9977107374 | ||
|
|
2690c3019d | ||
|
|
2a1a7326d0 | ||
|
|
f33ca95bdf | ||
|
|
bb23e88ead | ||
|
|
810fbdfe49 | ||
|
|
e204ff30b4 | ||
|
|
501145c5d9 | ||
|
|
dca5f65e89 | ||
|
|
00f55ff3ae | ||
|
|
52c70052a4 | ||
|
|
7a0cf4fbb6 | ||
|
|
76bae3d926 | ||
|
|
53234d283e | ||
|
|
3e10f1749a | ||
|
|
6d7630e7c0 | ||
|
|
b6a54c0d09 | ||
|
|
9897698322 | ||
|
|
4ef49cc832 | ||
|
|
6ad383799d | ||
|
|
e0ea8c9a57 | ||
|
|
4fc0855ba1 | ||
|
|
f50818697f | ||
|
|
39f2ebd2d6 | ||
|
|
89ea34c94e | ||
|
|
92729701c3 | ||
|
|
7d4651db69 | ||
|
|
5db9ae5f98 | ||
|
|
4d8eadc491 | ||
|
|
3ca87fc45b | ||
|
|
21204dee69 | ||
|
|
cfc9d13cad | ||
|
|
5bb04dafdd | ||
|
|
23077040ce | ||
|
|
b99ff67e69 | ||
|
|
f183a9d1a2 | ||
|
|
fc6bab9381 | ||
|
|
170fde5e23 | ||
|
|
c2f54c1dbc | ||
|
|
d83aae6dc4 | ||
|
|
9a184a8f35 | ||
|
|
fd30b3e402 | ||
|
|
cf19477d4d | ||
|
|
5f5cbd07c6 | ||
|
|
0381b8b8cb | ||
|
|
9369ff14b8 | ||
|
|
7c3140e424 | ||
|
|
9e29aa7a76 | ||
|
|
604d5b5884 | ||
|
|
245a84d20b | ||
|
|
f1c8bbf8dd | ||
|
|
60d0a48a1a | ||
|
|
7ab53f3357 | ||
|
|
4e93eb440c | ||
|
|
94cf6788d8 | ||
|
|
3eda9799c5 | ||
|
|
f29e989aff | ||
|
|
182361e598 | ||
|
|
50f37fd022 | ||
|
|
5ea47d733b | ||
|
|
2822b7a593 | ||
|
|
82804ebc67 | ||
|
|
8aad64ffa7 | ||
|
|
9e82a0b3c3 | ||
|
|
6ea4cf2797 | ||
|
|
165a9671fd | ||
|
|
25ba200a5e | ||
|
|
32ae7d5385 | ||
|
|
005bab2fb8 | ||
|
|
06d1e9ed95 | ||
|
|
a56ee25581 | ||
|
|
dceaa56ade | ||
|
|
6159319172 | ||
|
|
8be1dd435c | ||
|
|
cac612b176 | ||
|
|
6568faaf4f | ||
|
|
ea70ec9453 | ||
|
|
f64782e5d4 | ||
|
|
a2e63aa2c8 | ||
|
|
78149bbb3c | ||
|
|
a846eed306 | ||
|
|
1a89135342 | ||
|
|
0a2ac25b1c | ||
|
|
aed58e49bc | ||
|
|
881fcc6abe | ||
|
|
b4d70a35b3 | ||
|
|
b6c64e56e5 | ||
|
|
3299c71b3d | ||
|
|
2c8164e54f | ||
|
|
02f8591a60 | ||
|
|
0a0d9d6294 | ||
|
|
a17311a88d | ||
|
|
472eb1d435 | ||
|
|
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 |
26 changed files with 1232 additions and 284 deletions
|
|
@ -11,7 +11,7 @@
|
||||||
v-for="(nav, navIdx) in navigation"
|
v-for="(nav, navIdx) in navigation"
|
||||||
:class="[
|
:class="[
|
||||||
'transition uppercase font-display font-semibold text-md',
|
'transition uppercase font-display font-semibold text-md',
|
||||||
navIdx === currentPageIndex
|
navIdx === currentNavigation
|
||||||
? 'text-zinc-100'
|
? 'text-zinc-100'
|
||||||
: 'text-zinc-400 hover:text-zinc-200',
|
: 'text-zinc-400 hover:text-zinc-200',
|
||||||
]"
|
]"
|
||||||
|
|
@ -78,7 +78,7 @@ const navigation: Array<NavigationItem> = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
const {currentNavigation} = useCurrentNavigationIndex(navigation);
|
||||||
|
|
||||||
const quickActions: Array<QuickActionNav> = [
|
const quickActions: Array<QuickActionNav> = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,7 @@ export const useCurrentNavigationIndex = (
|
||||||
currentNavigation.value = calculateCurrentNavIndex(to);
|
currentNavigation.value = calculateCurrentNavIndex(to);
|
||||||
});
|
});
|
||||||
|
|
||||||
return currentNavigation;
|
return {currentNavigation, recalculateNavigation: () => {
|
||||||
|
currentNavigation.value = calculateCurrentNavIndex(route);
|
||||||
|
}};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row h-full">
|
<div class="flex flex-row h-full">
|
||||||
<div
|
<div class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1">
|
||||||
class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"
|
|
||||||
>
|
|
||||||
<ul class="flex flex-col gap-y-1">
|
<ul class="flex flex-col gap-y-1">
|
||||||
<NuxtLink
|
<NuxtLink v-for="(nav, navIdx) in navigation" :key="nav.route" :class="[
|
||||||
v-for="(nav, navIdx) in navigation"
|
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
|
||||||
:key="nav.route"
|
navIdx === currentNavigation
|
||||||
:class="[
|
? 'bg-zinc-800 text-zinc-100'
|
||||||
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
|
: nav.isInstalled.value
|
||||||
navIdx === currentNavigationIndex
|
|
||||||
? 'bg-zinc-800 text-zinc-100'
|
|
||||||
: nav.isInstalled.value
|
|
||||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||||
]"
|
]" :href="nav.route">
|
||||||
:href="nav.route"
|
|
||||||
>
|
|
||||||
<div class="flex items-center w-full gap-x-3">
|
<div class="flex items-center w-full gap-x-3">
|
||||||
<img
|
<img class="size-6 flex-none object-cover bg-zinc-900 rounded" :src="icons[nav.id]" alt="" />
|
||||||
class="size-6 flex-none object-cover bg-zinc-900 rounded"
|
|
||||||
:src="icons[navIdx]"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<p class="truncate text-sm font-display leading-6 flex-1">
|
<p class="truncate text-sm font-display leading-6 flex-1">
|
||||||
{{ nav.label }}
|
{{ nav.label }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -31,34 +20,47 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow overflow-y-auto">
|
<div class="grow overflow-y-auto">
|
||||||
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
|
<NuxtPage :libraryDownloadError="libraryDownloadError" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { GameStatusEnum, type Game, type GameStatus, type NavigationItem } from "~/types";
|
||||||
|
|
||||||
let libraryDownloadError = false;
|
let libraryDownloadError = false;
|
||||||
|
|
||||||
async function calculateGames(): Promise<Game[]> {
|
const games: { [key: string]: { game: Game, status: Ref<GameStatus, GameStatus> } } = {};
|
||||||
|
const icons: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
const rawGames: Ref<Game[], Game[]> = ref([])
|
||||||
|
|
||||||
|
async function calculateGames() {
|
||||||
try {
|
try {
|
||||||
return await invoke("fetch_library");
|
rawGames.value = await invoke("fetch_library");
|
||||||
|
for (const game of rawGames.value) {
|
||||||
|
if (games[game.id]) continue;
|
||||||
|
games[game.id] = await useGame(game.id);
|
||||||
|
}
|
||||||
|
for (const game of rawGames.value) {
|
||||||
|
if (icons[game.id]) continue;
|
||||||
|
icons[game.id] = await useObject(game.mIconId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
libraryDownloadError = true;
|
libraryDownloadError = true;
|
||||||
return new Array();
|
return new Array();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawGames: Array<Game> = await calculateGames();
|
await calculateGames();
|
||||||
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
|
|
||||||
const icons = await Promise.all(
|
const navigation = computed(() => rawGames.value.map((game) => {
|
||||||
games.map(({ game, status }) => useObject(game.mIconId))
|
const status = games[game.id].status;
|
||||||
);
|
|
||||||
|
|
||||||
const navigation = games.map(({ game, status }) => {
|
|
||||||
const isInstalled = computed(
|
const isInstalled = computed(
|
||||||
() =>
|
() =>
|
||||||
status.value.type == GameStatusEnum.Installed ||
|
status.value.type == GameStatusEnum.Installed ||
|
||||||
|
|
@ -70,9 +72,18 @@ const navigation = games.map(({ game, status }) => {
|
||||||
route: `/library/${game.id}`,
|
route: `/library/${game.id}`,
|
||||||
prefix: `/library/${game.id}`,
|
prefix: `/library/${game.id}`,
|
||||||
isInstalled,
|
isInstalled,
|
||||||
|
id: game.id,
|
||||||
};
|
};
|
||||||
return item;
|
return item;
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(navigation.value);
|
||||||
|
|
||||||
|
|
||||||
|
listen("update_library", async (event) => {
|
||||||
|
console.log("Updating library");
|
||||||
|
await calculateGames()
|
||||||
|
recalculateNavigation();
|
||||||
|
})
|
||||||
|
|
||||||
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@
|
||||||
<ul role="list" class="-mx-2 space-y-1">
|
<ul role="list" class="-mx-2 space-y-1">
|
||||||
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
|
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
|
||||||
<NuxtLink :href="item.route" :class="[
|
<NuxtLink :href="item.route" :class="[
|
||||||
itemIdx === currentPageIndex
|
itemIdx === currentNavigation
|
||||||
? 'bg-zinc-800/50 text-zinc-100'
|
? 'bg-zinc-800/50 text-zinc-100'
|
||||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
: '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',
|
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||||
]">
|
]">
|
||||||
<component :is="item.icon" :class="[
|
<component :is="item.icon" :class="[
|
||||||
itemIdx === currentPageIndex
|
itemIdx === currentNavigation
|
||||||
? 'text-zinc-100'
|
? 'text-zinc-100'
|
||||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||||
'transition h-6 w-6 shrink-0',
|
'transition h-6 w-6 shrink-0',
|
||||||
|
|
@ -112,10 +112,10 @@ const navigation = computed(() => [
|
||||||
const currentPlatform = platform();
|
const currentPlatform = platform();
|
||||||
|
|
||||||
// Use .value to unwrap the computed ref
|
// Use .value to unwrap the computed ref
|
||||||
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
|
const {currentNavigation} = useCurrentNavigationIndex(navigation.value);
|
||||||
|
|
||||||
// Watch for navigation changes and update currentPageIndex
|
// Watch for navigation changes and update currentPageIndex
|
||||||
watch(navigation, (newNav) => {
|
watch(navigation, (newNav) => {
|
||||||
currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
|
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="border-b border-zinc-600 py-2 px-1">
|
<div class="border-b border-zinc-600 py-2 px-1">
|
||||||
<div
|
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
||||||
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
|
|
||||||
>
|
|
||||||
<div class="ml-4 mt-2">
|
<div class="ml-4 mt-2">
|
||||||
<h3 class="text-base font-display font-semibold text-zinc-100">
|
<h3 class="text-base font-display font-semibold text-zinc-100">
|
||||||
Install directories
|
Install directories
|
||||||
|
|
@ -15,27 +13,17 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 mt-2 shrink-0">
|
<div class="ml-4 mt-2 shrink-0">
|
||||||
<button
|
<button @click="() => (open = true)" type="button"
|
||||||
@click="() => (open = true)"
|
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 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">
|
||||||
type="button"
|
|
||||||
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 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"
|
|
||||||
>
|
|
||||||
Add new directory
|
Add new directory
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul role="list" class="divide-y divide-gray-800">
|
<ul role="list" class="divide-y divide-gray-800">
|
||||||
<li
|
<li v-for="(dir, dirIdx) in dirs" :key="dir" class="flex justify-between gap-x-6 py-5">
|
||||||
v-for="(dir, dirIdx) in dirs"
|
|
||||||
:key="dir"
|
|
||||||
class="flex justify-between gap-x-6 py-5"
|
|
||||||
>
|
|
||||||
<div class="flex min-w-0 gap-x-4">
|
<div class="flex min-w-0 gap-x-4">
|
||||||
<FolderIcon
|
<FolderIcon class="h-6 w-6 text-blue-600 flex-none rounded-full" alt="" />
|
||||||
class="h-6 w-6 text-blue-600 flex-none rounded-full"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
<p class="text-sm/6 text-zinc-100">
|
<p class="text-sm/6 text-zinc-100">
|
||||||
{{ dir }}
|
{{ dir }}
|
||||||
|
|
@ -43,16 +31,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 items-center gap-x-6">
|
<div class="flex shrink-0 items-center gap-x-6">
|
||||||
<button
|
<button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[
|
||||||
@click="() => deleteDirectory(dirIdx)"
|
dirs.length <= 1
|
||||||
:disabled="dirs.length <= 1"
|
? 'text-zinc-700'
|
||||||
:class="[
|
: 'text-zinc-400 hover:text-zinc-100',
|
||||||
dirs.length <= 1
|
'-m-2.5 block p-2.5',
|
||||||
? 'text-zinc-700'
|
]">
|
||||||
: 'text-zinc-400 hover:text-zinc-100',
|
|
||||||
'-m-2.5 block p-2.5',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Open options</span>
|
<span class="sr-only">Open options</span>
|
||||||
<TrashIcon class="size-5" aria-hidden="true" />
|
<TrashIcon class="size-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -72,37 +56,44 @@
|
||||||
Maximum Download Threads
|
Maximum Download Threads
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads"
|
||||||
type="number"
|
@keypress="validateNumberInput" @paste="validatePaste"
|
||||||
name="threads"
|
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" />
|
||||||
id="threads"
|
|
||||||
min="1"
|
|
||||||
max="32"
|
|
||||||
v-model="downloadThreads"
|
|
||||||
@keypress="validateNumberInput"
|
|
||||||
@paste="validatePaste"
|
|
||||||
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-zinc-400">
|
<p class="mt-2 text-sm text-zinc-400">
|
||||||
The maximum number of concurrent download threads. Higher values may
|
The maximum number of concurrent download threads. Higher values may
|
||||||
download faster but use more system resources. Default is 4.
|
download faster but use more system resources. Default is 4.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<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">Force Offline</h3>
|
||||||
|
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||||
|
Drop will not make any external connections
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="forceOffline" :class="[
|
||||||
|
forceOffline ? '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="[
|
||||||
|
forceOffline ? '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 class="mt-6">
|
<div class="mt-6">
|
||||||
<button
|
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
|
||||||
type="button"
|
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
||||||
@click="saveDownloadThreads"
|
saveState.success
|
||||||
:disabled="saveState.loading"
|
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||||
:class="[
|
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
||||||
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
||||||
saveState.success
|
]">
|
||||||
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
|
||||||
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
|
||||||
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,49 +101,27 @@
|
||||||
</div>
|
</div>
|
||||||
<TransitionRoot as="template" :show="open">
|
<TransitionRoot as="template" :show="open">
|
||||||
<Dialog class="relative z-50" @close="open = false">
|
<Dialog class="relative z-50" @close="open = false">
|
||||||
<TransitionChild
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
||||||
as="template"
|
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
enter="ease-out duration-300"
|
<div class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity" />
|
||||||
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>
|
</TransitionChild>
|
||||||
|
|
||||||
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
<div
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
|
<TransitionChild as="template" enter="ease-out duration-300"
|
||||||
>
|
|
||||||
<TransitionChild
|
|
||||||
as="template"
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||||
leave="ease-in duration-200"
|
|
||||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
>
|
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
|
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||||
>
|
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mt-3 w-full sm:ml-4 sm:mt-0">
|
<div class="mt-3 w-full sm:ml-4 sm:mt-0">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label for="dir" class="block text-sm/6 font-medium text-zinc-100">Select game directory</label>
|
||||||
for="dir"
|
|
||||||
class="block text-sm/6 font-medium text-zinc-100"
|
|
||||||
>Select game directory</label
|
|
||||||
>
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<button
|
<button @click="() => selectDirectory()"
|
||||||
@click="() => selectDirectory()"
|
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6">
|
||||||
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6"
|
|
||||||
>
|
|
||||||
{{
|
{{
|
||||||
currentDirectory ?? "Click to select a directory..."
|
currentDirectory ?? "Click to select a directory..."
|
||||||
}}
|
}}
|
||||||
|
|
@ -165,36 +134,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<LoadingButton
|
<LoadingButton :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading"
|
||||||
:disabled="currentDirectory == undefined"
|
@click="() => submitDirectory()" :class="[
|
||||||
type="button"
|
|
||||||
:loading="createDirectoryLoading"
|
|
||||||
@click="() => submitDirectory()"
|
|
||||||
:class="[
|
|
||||||
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
|
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
|
||||||
currentDirectory === undefined
|
currentDirectory === undefined
|
||||||
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
|
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
|
||||||
: 'text-white bg-blue-600 hover:bg-blue-500',
|
: 'text-white bg-blue-600 hover:bg-blue-500',
|
||||||
]"
|
]">
|
||||||
>
|
|
||||||
Add
|
Add
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
<button
|
<button type="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-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
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-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||||
@click="() => cancelDirectory()"
|
@click="() => cancelDirectory()" ref="cancelButtonRef">
|
||||||
ref="cancelButtonRef"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
|
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<XCircleIcon
|
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||||
class="h-5 w-5 text-red-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-red-600">
|
<h3 class="text-sm font-medium text-red-600">
|
||||||
|
|
@ -220,6 +178,7 @@ import {
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { Switch } from '@headlessui/vue'
|
||||||
import { type Settings } from "~/types";
|
import { type Settings } from "~/types";
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
|
|
@ -231,6 +190,7 @@ const dirs = ref<Array<string>>([]);
|
||||||
|
|
||||||
const settings = await invoke<Settings>("fetch_settings");
|
const settings = await invoke<Settings>("fetch_settings");
|
||||||
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
||||||
|
const forceOffline = ref(settings?.forceOffline ?? false);
|
||||||
|
|
||||||
const saveState = reactive({
|
const saveState = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
@ -293,21 +253,21 @@ async function deleteDirectory(index: number) {
|
||||||
await updateDirs();
|
await updateDirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDownloadThreads() {
|
async function saveSettings() {
|
||||||
try {
|
try {
|
||||||
saveState.loading = true;
|
saveState.loading = true;
|
||||||
await invoke("update_settings", {
|
await invoke("update_settings", {
|
||||||
newSettings: { maxDownloadThreads: downloadThreads.value },
|
newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success state
|
// Show success state
|
||||||
saveState.success = true;
|
saveState.success = true;
|
||||||
|
|
||||||
// Reset back to normal state after 2 seconds
|
// Reset back to normal state after 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveState.success = false;
|
saveState.success = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save settings:', error);
|
console.error('Failed to save settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -317,8 +277,8 @@ async function saveDownloadThreads() {
|
||||||
|
|
||||||
function validateNumberInput(event: KeyboardEvent) {
|
function validateNumberInput(event: KeyboardEvent) {
|
||||||
// Allow only numbers and basic control keys
|
// Allow only numbers and basic control keys
|
||||||
if (!/^\d$/.test(event.key) &&
|
if (!/^\d$/.test(event.key) &&
|
||||||
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
926
src-tauri/Cargo.lock
generated
926
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -50,6 +50,11 @@ slice-deque = "0.3.0"
|
||||||
throttle_my_fn = "0.2.6"
|
throttle_my_fn = "0.2.6"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
atomic-instant-full = "0.1.0"
|
atomic-instant-full = "0.1.0"
|
||||||
|
cacache = "13.1.0"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
http-serde = "2.1.1"
|
||||||
|
reqwest-middleware = "0.4.0"
|
||||||
|
reqwest-middleware-cache = "0.1.1"
|
||||||
|
|
||||||
[dependencies.tauri]
|
[dependencies.tauri]
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@ pub fn update_settings(new_settings: Value) {
|
||||||
}
|
}
|
||||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
||||||
db_lock.settings = new_settings;
|
db_lock.settings = new_settings;
|
||||||
println!("new Settings: {:?}", db_lock.settings);
|
drop(db_lock);
|
||||||
|
save_db();
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_settings() -> Settings {
|
pub fn fetch_settings() -> Settings {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ pub enum GameDownloadStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stuff that shouldn't be synced to disk
|
// Stuff that shouldn't be synced to disk
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub enum ApplicationTransientStatus {
|
pub enum ApplicationTransientStatus {
|
||||||
Downloading { version_name: String },
|
Downloading { version_name: String },
|
||||||
Uninstalling {},
|
Uninstalling {},
|
||||||
|
|
@ -98,9 +98,10 @@ pub struct Database {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub applications: DatabaseApplications,
|
pub applications: DatabaseApplications,
|
||||||
pub prev_database: Option<PathBuf>,
|
pub prev_database: Option<PathBuf>,
|
||||||
|
pub cache_dir: PathBuf
|
||||||
}
|
}
|
||||||
impl Database {
|
impl Database {
|
||||||
fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
|
fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>, cache_dir: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
applications: DatabaseApplications {
|
applications: DatabaseApplications {
|
||||||
install_dirs: vec![games_base_dir.into()],
|
install_dirs: vec![games_base_dir.into()],
|
||||||
|
|
@ -112,10 +113,8 @@ impl Database {
|
||||||
prev_database,
|
prev_database,
|
||||||
base_url: "".to_owned(),
|
base_url: "".to_owned(),
|
||||||
auth: None,
|
auth: None,
|
||||||
settings: Settings {
|
settings: Settings::default(),
|
||||||
autostart: false,
|
cache_dir,
|
||||||
max_download_threads: 4,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,21 +149,23 @@ impl DatabaseImpls for DatabaseInterface {
|
||||||
let db_path = data_root_dir.join("drop.db");
|
let db_path = data_root_dir.join("drop.db");
|
||||||
let games_base_dir = data_root_dir.join("games");
|
let games_base_dir = data_root_dir.join("games");
|
||||||
let logs_root_dir = data_root_dir.join("logs");
|
let logs_root_dir = data_root_dir.join("logs");
|
||||||
|
let cache_dir = data_root_dir.join("cache");
|
||||||
|
|
||||||
debug!("creating data directory at {:?}", data_root_dir);
|
debug!("creating data directory at {:?}", data_root_dir);
|
||||||
create_dir_all(data_root_dir.clone()).unwrap();
|
create_dir_all(data_root_dir.clone()).unwrap();
|
||||||
create_dir_all(games_base_dir.clone()).unwrap();
|
create_dir_all(&games_base_dir).unwrap();
|
||||||
create_dir_all(logs_root_dir.clone()).unwrap();
|
create_dir_all(&logs_root_dir).unwrap();
|
||||||
|
create_dir_all(&cache_dir).unwrap();
|
||||||
|
|
||||||
let exists = fs::exists(db_path.clone()).unwrap();
|
let exists = fs::exists(db_path.clone()).unwrap();
|
||||||
|
|
||||||
match exists {
|
match exists {
|
||||||
true => match PathDatabase::load_from_path(db_path.clone()) {
|
true => match PathDatabase::load_from_path(db_path.clone()) {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
|
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||||
},
|
},
|
||||||
false => {
|
false => {
|
||||||
let default = Database::new(games_base_dir, None);
|
let default = Database::new(games_base_dir, None, cache_dir);
|
||||||
debug!(
|
debug!(
|
||||||
"Creating database at path {}",
|
"Creating database at path {}",
|
||||||
db_path.as_os_str().to_str().unwrap()
|
db_path.as_os_str().to_str().unwrap()
|
||||||
|
|
@ -204,6 +205,7 @@ fn handle_invalid_database(
|
||||||
_e: RustbreakError,
|
_e: RustbreakError,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
games_base_dir: PathBuf,
|
games_base_dir: PathBuf,
|
||||||
|
cache_dir: PathBuf
|
||||||
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||||
let new_path = {
|
let new_path = {
|
||||||
let time = Utc::now().timestamp();
|
let time = Utc::now().timestamp();
|
||||||
|
|
@ -220,6 +222,7 @@ fn handle_invalid_database(
|
||||||
let db = Database::new(
|
let db = Database::new(
|
||||||
games_base_dir.into_os_string().into_string().unwrap(),
|
games_base_dir.into_os_string().into_string().unwrap(),
|
||||||
Some(new_path),
|
Some(new_path),
|
||||||
|
cache_dir
|
||||||
);
|
);
|
||||||
|
|
||||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub autostart: bool,
|
pub autostart: bool,
|
||||||
pub max_download_threads: usize,
|
pub max_download_threads: usize,
|
||||||
|
pub force_offline: bool
|
||||||
// ... other settings ...
|
// ... other settings ...
|
||||||
}
|
}
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
|
|
@ -12,6 +13,7 @@ impl Default for Settings {
|
||||||
Self {
|
Self {
|
||||||
autostart: false,
|
autostart: false,
|
||||||
max_download_threads: 4,
|
max_download_threads: 4,
|
||||||
|
force_offline: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ pub enum DownloadManagerSignal {
|
||||||
Uninstall(DownloadableMetadata),
|
Uninstall(DownloadableMetadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub enum DownloadManagerStatus {
|
pub enum DownloadManagerStatus {
|
||||||
Downloading,
|
Downloading,
|
||||||
Paused,
|
Paused,
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ impl DownloadManagerBuilder {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||||
download_agent.on_error(&app_handle, e.clone());
|
download_agent.on_error(&app_handle, &e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +287,7 @@ impl DownloadManagerBuilder {
|
||||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||||
debug!("got signal Error");
|
debug!("got signal Error");
|
||||||
if let Some(current_agent) = self.current_download_agent.clone() {
|
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||||
current_agent.on_error(&self.app_handle, error.clone());
|
current_agent.on_error(&self.app_handle, &error);
|
||||||
|
|
||||||
self.stop_and_wait_current_download();
|
self.stop_and_wait_current_download();
|
||||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub trait Downloadable: Send + Sync {
|
||||||
fn status(&self) -> DownloadStatus;
|
fn status(&self) -> DownloadStatus;
|
||||||
fn metadata(&self) -> DownloadableMetadata;
|
fn metadata(&self) -> DownloadableMetadata;
|
||||||
fn on_initialised(&self, app_handle: &AppHandle);
|
fn on_initialised(&self, app_handle: &AppHandle);
|
||||||
fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError);
|
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||||
fn on_complete(&self, app_handle: &AppHandle);
|
fn on_complete(&self, app_handle: &AppHandle);
|
||||||
fn on_incomplete(&self, app_handle: &AppHandle);
|
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use serde_with::SerializeDisplay;
|
||||||
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
|
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
|
||||||
|
|
||||||
// TODO: Rename / separate from downloads
|
// TODO: Rename / separate from downloads
|
||||||
#[derive(Debug, Clone, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum ApplicationDownloadError {
|
pub enum ApplicationDownloadError {
|
||||||
Communication(RemoteAccessError),
|
Communication(RemoteAccessError),
|
||||||
Checksum,
|
Checksum,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use url::ParseError;
|
||||||
|
|
||||||
use super::drop_server_error::DropServerError;
|
use super::drop_server_error::DropServerError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum RemoteAccessError {
|
pub enum RemoteAccessError {
|
||||||
FetchError(Arc<reqwest::Error>),
|
FetchError(Arc<reqwest::Error>),
|
||||||
ParsingError(ParseError),
|
ParsingError(ParseError),
|
||||||
|
|
@ -21,6 +21,7 @@ pub enum RemoteAccessError {
|
||||||
InvalidRedirect,
|
InvalidRedirect,
|
||||||
ManifestDownloadFailed(StatusCode, String),
|
ManifestDownloadFailed(StatusCode, String),
|
||||||
OutOfSync,
|
OutOfSync,
|
||||||
|
Cache(cacache::Error),
|
||||||
Generic(String),
|
Generic(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ impl Display for RemoteAccessError {
|
||||||
),
|
),
|
||||||
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::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),
|
RemoteAccessError::Generic(message) => write!(f, "{}", message),
|
||||||
|
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use tauri::AppHandle;
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
|
database::db::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, AppState
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
@ -15,16 +15,16 @@ use super::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
pub fn fetch_library(state: tauri::State<'_, Mutex<AppState>>) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
fetch_library_logic(app)
|
offline!(state, fetch_library_logic, fetch_library_logic_offline, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_game(
|
pub fn fetch_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
app: tauri::AppHandle,
|
state: tauri::State<'_, Mutex<AppState>>
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
fetch_game_logic(game_id, app)
|
offline!(state, fetch_game_logic, fetch_game_logic_offline, game_id, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -38,7 +38,6 @@ pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), Libr
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||||
};
|
};
|
||||||
println!("{:?}", meta);
|
|
||||||
uninstall_game_logic(meta, &app_handle);
|
uninstall_game_logic(meta, &app_handle);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl GameDownloadAgent {
|
||||||
&[("id", &self.id), ("version", &self.version)],
|
&[("id", &self.id), ("version", &self.version)],
|
||||||
|f| f.header("Authorization", header),
|
|f| f.header("Authorization", header),
|
||||||
)
|
)
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e))?
|
.map_err(ApplicationDownloadError::Communication)?
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
|
|
@ -280,9 +280,13 @@ impl GameDownloadAgent {
|
||||||
) {
|
) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
|
sender
|
||||||
|
.send(DownloadManagerSignal::Error(
|
||||||
|
ApplicationDownloadError::Communication(e),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.spawn(move |_| {
|
scope.spawn(move |_| {
|
||||||
|
|
@ -362,7 +366,7 @@ impl Downloadable for GameDownloadAgent {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
|
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||||
app_handle
|
app_handle
|
||||||
.emit("download_error", error.to_string())
|
.emit("download_error", error.to_string())
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,17 @@ use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
use crate::remote::auth::generate_authorization_header;
|
use crate::remote::auth::generate_authorization_header;
|
||||||
|
use crate::remote::cache::{cache_object, get_cached_object};
|
||||||
use crate::remote::requests::make_request;
|
use crate::remote::requests::make_request;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct FetchGameStruct {
|
pub struct FetchGameStruct {
|
||||||
game: Game,
|
game: Game,
|
||||||
status: GameStatusWithTransient,
|
status: GameStatusWithTransient,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
id: String,
|
id: String,
|
||||||
|
|
@ -66,7 +67,9 @@ pub struct StatsUpdateEvent {
|
||||||
pub time: usize,
|
pub time: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
|
pub fn fetch_library_logic(
|
||||||
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
@ -83,7 +86,6 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
|
||||||
|
|
||||||
let games: Vec<Game> = response.json()?;
|
let games: Vec<Game> = response.json()?;
|
||||||
|
|
||||||
let state = app.state::<Mutex<AppState>>();
|
|
||||||
let mut handle = state.lock().unwrap();
|
let mut handle = state.lock().unwrap();
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
|
@ -97,17 +99,32 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
|
||||||
.insert(game.id.clone(), GameDownloadStatus::Remote {});
|
.insert(game.id.clone(), GameDownloadStatus::Remote {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(handle);
|
drop(handle);
|
||||||
|
drop(db_handle);
|
||||||
|
cache_object("library", &games)?;
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
|
pub fn fetch_library_logic_offline(
|
||||||
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
|
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||||
|
|
||||||
|
let db_handle = borrow_db_checked();
|
||||||
|
|
||||||
|
games.retain(|game| {
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.contains_key(&game.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(games)
|
||||||
|
}
|
||||||
pub fn fetch_game_logic(
|
pub fn fetch_game_logic(
|
||||||
id: String,
|
id: String,
|
||||||
app: tauri::AppHandle,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let state = app.state::<Mutex<AppState>>();
|
|
||||||
let mut state_handle = state.lock().unwrap();
|
let mut state_handle = state.lock().unwrap();
|
||||||
|
|
||||||
let game = state_handle.games.get(&id);
|
let game = state_handle.games.get(&id);
|
||||||
|
|
@ -119,6 +136,8 @@ pub fn fetch_game_logic(
|
||||||
status,
|
status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cache_object(id, &data)?;
|
||||||
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
@ -155,9 +174,18 @@ pub fn fetch_game_logic(
|
||||||
status,
|
status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cache_object(id, &data)?;
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fetch_game_logic_offline(
|
||||||
|
id: String,
|
||||||
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
|
get_cached_object::<String, FetchGameStruct>(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fetch_game_verion_options_logic(
|
pub fn fetch_game_verion_options_logic(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
|
|
@ -193,7 +221,7 @@ pub fn fetch_game_verion_options_logic(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||||
println!("triggered uninstall for agent");
|
debug!("triggered uninstall for agent");
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
|
|
@ -213,6 +241,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let previous_state = previous_state.unwrap();
|
let previous_state = previous_state.unwrap();
|
||||||
|
|
||||||
if let Some((_, install_dir)) = match previous_state {
|
if let Some((_, install_dir)) = match previous_state {
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
version_name,
|
version_name,
|
||||||
|
|
@ -229,6 +258,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
.entry(meta.clone())
|
.entry(meta.clone())
|
||||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||||
|
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
|
|
@ -239,6 +269,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.applications.transient_statuses.remove(&meta);
|
db_handle.applications.transient_statuses.remove(&meta);
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.remove(&meta.id);
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
|
|
@ -248,6 +282,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||||
save_db();
|
save_db();
|
||||||
|
|
||||||
debug!("uninstalled game id {}", &meta.id);
|
debug!("uninstalled game id {}", &meta.id);
|
||||||
|
app_handle.emit("update_library", {}).unwrap();
|
||||||
|
|
||||||
push_game_update(
|
push_game_update(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,13 @@ use log4rs::encode::pattern::PatternEncoder;
|
||||||
use log4rs::Config;
|
use log4rs::Config;
|
||||||
use process::commands::{kill_game, launch_game};
|
use process::commands::{kill_game, launch_game};
|
||||||
use process::process_manager::ProcessManager;
|
use process::process_manager::ProcessManager;
|
||||||
use remote::auth::{self, generate_authorization_header, recieve_handshake};
|
use remote::auth::{self, recieve_handshake};
|
||||||
use remote::commands::{
|
use remote::commands::{
|
||||||
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
|
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote
|
||||||
};
|
};
|
||||||
|
use remote::fetch_object::{fetch_object, fetch_object_offline};
|
||||||
use remote::requests::make_request;
|
use remote::requests::make_request;
|
||||||
|
use reqwest::blocking::Body;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -60,9 +62,10 @@ use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize)]
|
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
||||||
pub enum AppStatus {
|
pub enum AppStatus {
|
||||||
NotConfigured,
|
NotConfigured,
|
||||||
|
Offline,
|
||||||
ServerError,
|
ServerError,
|
||||||
SignedOut,
|
SignedOut,
|
||||||
SignedIn,
|
SignedIn,
|
||||||
|
|
@ -236,6 +239,7 @@ pub fn run() {
|
||||||
// Remote
|
// Remote
|
||||||
use_remote,
|
use_remote,
|
||||||
gen_drop_url,
|
gen_drop_url,
|
||||||
|
fetch_drop_object,
|
||||||
// Library
|
// Library
|
||||||
fetch_library,
|
fetch_library,
|
||||||
fetch_game,
|
fetch_game,
|
||||||
|
|
@ -327,7 +331,7 @@ pub fn run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("menu event not handled: {:?}", event.id);
|
warn!("menu event not handled: {:?}", event.id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(app)
|
.build(app)
|
||||||
|
|
@ -356,35 +360,9 @@ pub fn run() {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("object", move |ctx, request, responder| {
|
||||||
// Drop leading /
|
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||||
let object_id = &request.uri().path()[1..];
|
offline!(state, fetch_object, fetch_object_offline, request, responder);
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
|
||||||
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
|
||||||
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
|
|
||||||
f.header("Authorization", header)
|
|
||||||
})
|
|
||||||
.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,
|
|
||||||
response.headers().get("Content-Type").unwrap(),
|
|
||||||
);
|
|
||||||
let data = Vec::from(response.bytes().unwrap());
|
|
||||||
let resp = resp_builder.body(data).unwrap();
|
|
||||||
|
|
||||||
responder.respond(resp);
|
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ impl ProcessHandler for UMULauncher {
|
||||||
_log_file: File,
|
_log_file: File,
|
||||||
_error_file: File,
|
_error_file: File,
|
||||||
) -> Result<Child, Error> {
|
) -> Result<Child, Error> {
|
||||||
println!("Game override: .{:?}.", &game_version.umu_id_override);
|
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
|
||||||
let game_id = match &game_version.umu_id_override {
|
let game_id = match &game_version.umu_id_override {
|
||||||
Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
|
Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
|
||||||
None => game_version.game_id.clone()
|
None => game_version.game_id.clone()
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ pub fn setup() -> (AppStatus, Option<User>) {
|
||||||
if auth.is_some() {
|
if auth.is_some() {
|
||||||
let user_result = match fetch_user() {
|
let user_result = match fetch_user() {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
|
Err(RemoteAccessError::FetchError(_)) => return (AppStatus::Offline, None),
|
||||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||||
};
|
};
|
||||||
return (AppStatus::SignedIn, Some(user_result));
|
return (AppStatus::SignedIn, Some(user_result));
|
||||||
|
|
|
||||||
63
src-tauri/src/remote/cache.rs
Normal file
63
src-tauri/src/remote/cache.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::{database::db::borrow_db_checked, error::remote_access_error::RemoteAccessError};
|
||||||
|
use cacache::Integrity;
|
||||||
|
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use tauri::{UriSchemeContext, UriSchemeResponder};
|
||||||
|
|
||||||
|
use super::{auth::generate_authorization_header, requests::make_request};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! offline {
|
||||||
|
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||||
|
|
||||||
|
if crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == crate::AppStatus::Offline {
|
||||||
|
$func2( $( $arg ), *)
|
||||||
|
} else {
|
||||||
|
$func1( $( $arg ), *)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
|
||||||
|
key: K,
|
||||||
|
data: &D,
|
||||||
|
) -> Result<Integrity, RemoteAccessError> {
|
||||||
|
let bytes = serde_json::to_vec(data).unwrap();
|
||||||
|
cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
|
||||||
|
.map_err(|e| RemoteAccessError::Cache(e))
|
||||||
|
}
|
||||||
|
pub fn get_cached_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
|
||||||
|
key: K,
|
||||||
|
) -> Result<D, RemoteAccessError> {
|
||||||
|
let bytes = cacache::read_sync(&borrow_db_checked().cache_dir, key)
|
||||||
|
.map_err(|e| RemoteAccessError::Cache(e))?;
|
||||||
|
let data = serde_json::from_slice::<D>(&bytes).unwrap();
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ObjectCache {
|
||||||
|
content_type: String,
|
||||||
|
body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Response<Vec<u8>>> for ObjectCache {
|
||||||
|
fn from(value: Response<Vec<u8>>) -> Self {
|
||||||
|
ObjectCache {
|
||||||
|
content_type: value
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned(),
|
||||||
|
body: value.body().clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<ObjectCache> for Response<Vec<u8>> {
|
||||||
|
fn from(value: ObjectCache) -> Self {
|
||||||
|
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
|
||||||
|
resp_builder.body(value.body).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, AppState, AppStatus
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
AppState, AppStatus,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::{auth_initiate_logic, recieve_handshake, setup},
|
auth::{auth_initiate_logic, recieve_handshake, setup}, cache::{cache_object, get_cached_object}, remote::use_remote_logic
|
||||||
remote::use_remote_logic,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -35,6 +34,28 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||||
Ok(url.to_string())
|
Ok(url.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||||
|
let drop_url = gen_drop_url(path.clone());
|
||||||
|
let req = make_request(
|
||||||
|
&Client::new(),
|
||||||
|
&[&path],
|
||||||
|
&[],
|
||||||
|
|r| { r.header("Authorization", generate_authorization_header()) }
|
||||||
|
)?.send();
|
||||||
|
|
||||||
|
match req {
|
||||||
|
Ok(data) => {
|
||||||
|
let data = data.bytes()?.to_vec();
|
||||||
|
cache_object(&path, &data)?;
|
||||||
|
Ok(data)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
debug!("{}", e);
|
||||||
|
get_cached_object::<&str, Vec<u8>>(&path)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn sign_out(app: AppHandle) {
|
pub fn sign_out(app: AppHandle) {
|
||||||
// Clear auth from database
|
// Clear auth from database
|
||||||
|
|
|
||||||
55
src-tauri/src/remote/fetch_object.rs
Normal file
55
src-tauri/src/remote/fetch_object.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||||
|
use log::warn;
|
||||||
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
|
use super::{auth::generate_authorization_header, cache::{cache_object, get_cached_object, ObjectCache}, requests::make_request};
|
||||||
|
|
||||||
|
pub fn fetch_object(
|
||||||
|
request: http::Request<Vec<u8>>,
|
||||||
|
responder: UriSchemeResponder,
|
||||||
|
) {
|
||||||
|
// Drop leading /
|
||||||
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
|
let header = generate_authorization_header();
|
||||||
|
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
||||||
|
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
|
||||||
|
f.header("Authorization", header)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.send();
|
||||||
|
if response.is_err() {
|
||||||
|
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Ok(data) => responder.respond(data.into()),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{}", e)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = response.unwrap();
|
||||||
|
|
||||||
|
let resp_builder = ResponseBuilder::new().header(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
response.headers().get("Content-Type").unwrap(),
|
||||||
|
);
|
||||||
|
let data = Vec::from(response.bytes().unwrap());
|
||||||
|
let resp = resp_builder.body(data).unwrap();
|
||||||
|
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
|
||||||
|
|
||||||
|
responder.respond(resp);
|
||||||
|
}
|
||||||
|
pub fn fetch_object_offline(
|
||||||
|
request: http::Request<Vec<u8>>,
|
||||||
|
responder: UriSchemeResponder,
|
||||||
|
) {
|
||||||
|
let object_id = &request.uri().path()[1..];
|
||||||
|
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Ok(data) => responder.respond(data.into()),
|
||||||
|
Err(e) => warn!("{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod cache;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod fetch_object;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
|
|
|
||||||
2
types.ts
2
types.ts
|
|
@ -38,6 +38,7 @@ export type Game = {
|
||||||
|
|
||||||
export enum AppStatus {
|
export enum AppStatus {
|
||||||
NotConfigured = "NotConfigured",
|
NotConfigured = "NotConfigured",
|
||||||
|
Offline = "Offline",
|
||||||
SignedOut = "SignedOut",
|
SignedOut = "SignedOut",
|
||||||
SignedIn = "SignedIn",
|
SignedIn = "SignedIn",
|
||||||
SignedInNeedsReauth = "SignedInNeedsReauth",
|
SignedInNeedsReauth = "SignedInNeedsReauth",
|
||||||
|
|
@ -76,4 +77,5 @@ export type DownloadableMetadata = {
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
autostart: boolean,
|
autostart: boolean,
|
||||||
maxDownloadThreads: number,
|
maxDownloadThreads: number,
|
||||||
|
forceOffline: boolean
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue