Compare commits

..

4 commits

Author SHA1 Message Date
quexeky
9fab0af4e8
fix: Removed unnecessary nightly feature try_trait_v2 2025-01-28 22:48:26 +11:00
quexeky
3968a61786
fix(downloads): Fix rearranging download queue throwing error 2025-01-25 23:20:31 +11:00
quexeky
8861fe4e3d
chore: Version bump 2025-01-25 19:46:58 +11:00
quexeky
f5bd12b43a
Merge develop into main (#25)
* chore(process manager): refactor for generic way to implement cross
platform launchers

* feat(game): game uninstalling & partial compat

* chore(metadata): update metadata

* feat(errors): better download manager errors + modal

* feat(process): better process management, including running state

* feat(downloads): lockless tracking of downloaded chunks

* fix(sign on): add message about nonce expiration

* feat(download ui): add speed and time remaining information

closes #7

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>

* chore: Ran cargo clippy

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

* fix(auth initiate): add better error message

* feat(auth): offer manual signin

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* adds nvm rc!

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* fix(library page): fix install button

* fix(process): fix poorly designed parsing for executables with spaces

* fix(scrollbars): fix ugly scrollbars on edge webview

* feat(Compat): Implemented spawning with umu (using umu-wrapper-lib)

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

* feat(process manager): Game kill tauri command

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

* fix(deep links): Re-enabled deep links

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

* feat(process): shared child with stop command

* squash(autostart): added adenmgb's autostart feature

Squashed commit of the following:

commit 085cd9481dee748ee84c1a8f9dcd8ef0b01105ab
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:41 2024 +1030

    Update lib.rs for the DB sync of autostart

commit 86f2fb19bde3933ec6b5cd82701de0d306121659
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:13 2024 +1030

    Update db.rs to accomidate the settings sync

commit ece11e7581c9aef55588b2e829379b1224021a45
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:27:48 2024 +1030

    Update autostart.rs to include DB

commit 7ea8a24fdc2ff98379694ce8e347a40fcfd5fea3
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:38 2024 +1030

    Add files via upload

commit af2f232d94fa449d9e20df737ce81ebded87fd50
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:09 2024 +1030

    Delete src-tauri/Cargo.toml

commit 5d27b65612457de6eb30835d5423b9d96fd5a596
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:42 2024 +1030

    Add files via upload

commit 2eea7b97a876e23cc0d6daec37f1b75af9bf3ae2
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:31 2024 +1030

    Delete src-tauri/src/lib.rs

commit 9a635a10d1340f86c74812113284b115b34b9bbe
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:14:49 2024 +1030

    Add files via upload

commit 2fb049531a082fbdd217aba694819b7a3f954a55
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:37 2024 +1030

    Add files via upload

commit ea1be4d7505a9ab16bda338491c0ec313d0bc586
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:20 2024 +1030

    Delete pages/settings/index.vue

* fix(download manager): fix incorrect error assumptions & update types

* feat(account settings): Add signout functionality (#16)

* Create account.vue with logout button

* Update auth.rs to add signout command

* Update lib.rs to pass sign_out command to frontend

* feat(settings): add debug page

* Create debug.rs

* Update settings.vue to add tab for debug

* Update main.scss to add light theme

* Update interface.vue to add light mode

* Create debug.vue

* Update debug.vue too add open log button

* Update lib.rs

* Update debug.rs

* Update debug.rs

* Update lib.rs

* Update lib.rs

* Update debug.rs

* Update debug.vue

* fix(debug): refactor and cleanup

* revert(theme): revert light theming

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>

* feat(library ui): add installed ui in the library menu

* chore(tool manager): Progress on adding tools

Going to try changing around the download manager to take a generic trait rather than specifically for game downloads

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

* refactor(download manager): Moved download manager to separate directory

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

* refactor(download manager): Added Downloadable trait and replaced references to GameDownloadAgent

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

* chore(download manager): Renamed most instances of "game" outside of actual game downloads

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

* refactor(download manager): Renamed GameDonwloadError to ApplicationDownloadError and moved

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

* chore(download manager): Some easy cleanup of the download manager

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

* chore(download manager): Ensure that Downloadable is also send and sync

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

* refactor(download manager): Moved manifest and stored_manifest to download_manager

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

* Revert "refactor(download manager): Moved manifest and stored_manifest to download_manager"

This reverts commit 8db2393346.

* chore(tool manager): Added ToolDownloadAgent

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

* chore(download manager): Added manage_queue_signal

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

* chore(download manager): Added manage_go_signal command

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

* refactor(download manager): Removed all references to anything outside of the DownloadManager

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

* refactor(download manager): Fully separate & generic download manager

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

* refactor(download manager): Removed Arc requirement for DownloadableMetadata

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

* feat(download manager): Added generic download manager

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

* fix(game launcher): Renamed game_id to id

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

* fix(uninstalling): Re-enabled uninstalling apps

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

* refactor(downloads): Moved all files relevant to game downloads to their own directory

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

* fix(kill game): Re-enabled killing games

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

* feat(recovery): Added database recovery

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

* feat(database): Added database corruption dialog

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

* chore(README): Updated README.md

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

* perf(game downloads): Moved some variable declarations outside of the spawned download thread

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

* fix(game downloads): Accidentally was attempting to lock onto something that was already in scope

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

* fix(db): Added Settings component

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

* refactor: Ran cargo clippy & fmt

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

* chore: More cleanup after cargo clippy

Also added some type efficiency improvements (using references where possible and added SliceDeque crate)

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

* feat(settings): Added max_download_threads setting and separated settings from db

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

* chore: Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs

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

* chore: General cleanup

- Changed some info!() statements to debug!() and warn!()
- Removed most Turbofish syntax cases
- Removed InvalidCodeError and replaced it with InvalidResponse

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

* chore: Removed tests/

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

* chore: Removed tools/

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

* chore: More refining info!() statements

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

* feat(download manager): Added UI to change download threads

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Signed-off-by: quexeky <git@quexeky.dev>

* fix(metadata): update routes for new server

* fix(handle invalid database): use set_file_name instead of pushing to
strings

* refactor(compat): remove unnecessary compat code (#20)

* Delete pages/settings/compatibility.vue

* Update settings.vue

* Update debug.vue

* Update lib.rs

* Update compat.rs

* feat(debug): use shift or DEBUG RUST_LOG to show Debug Info

* Update settings.vue to have a conditional debug page

* Update debug.rs to add RUST_LOG status fetching

* Implement better error system and segregate errors and commands (#23)

* chore: Progress on amend_settings command

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

* chore(errors): Progress on better error handling with segragation of files

* chore: Progress on amend_settings command

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

* chore(commands): Separated commands under each subdirectory into respective commands.rs files

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

* chore(errors): Almost all errors and commands have been segregated

* chore(errors): Added drop server error

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

* feat(core): Update to using nightly compiler

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

* chore(errors): More progress on error handling

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

* chore(errors): Implementing Try and FromResidual for UserValue

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

* refactor(errors): Segregated errors and commands from code, and made commands return UserValue struct

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

* fix(errors): Added missing files

* chore(errors): Convert match statement to map_err

* feat(settings): Implemented settings editing from UI

* feat(errors): Clarified return values from retry_connect command

* chore(errors): Moved autostart commands to autostart.rs

* chore(process manager): Converted launch_process function for games to use game_id

---------

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

* fix(settings): Broken command invoke logic in settings/downloads.vue

* feat(logging): Added line numbers to file logging and highlighting to console

* chore(progress): Added rolling_progress_updates.rs

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

* chore(exit): Progress on cleanup and exit

* chore(downloads): Progress on terminator

* chore: Progress on rolling progress window

* feat(progress): Added rolling progress window

Still needs tweaks on specific timings, as well as cleanup

* refactor(remote): Created separate function to generate requests

* fix(install ui): stop loading on error

* fix: fix other metadata endpoints

* feat(errors): Using SerializeDisplay for better error management with Result

* chore: Update .gitlab-ci.yml

* refactor(logging): Using more appropriate logging statements

Still probably needs some work, but that's enough for now

* chore(logging): Imported appropriate logging macros

* Revert "chore: Update .gitlab-ci.yml"

This reverts commit fc6bab9381.

* feat(settings): Allow settings to update UI using fetch_settings command

* style(logging): Ensured that all logs start with lowercase capital and have no trailing punctuation

* fix(download manager): don't crash download manager if multiple errors
come in

* feat(downloads): re-enable checksums

* fix(logs): add file & line to console logs

* fix(ui): modal stack doesn't cover whole app

* feat(database): Ensure that any database issues are resolved by standalone functions

Functions are as follows:
- save_db()
- borrow_db_checked()
- borrow_db_mut_checked()

* chore: Ran cargo clippy & cargo fmt

* fix: assorted fixes

* fix(download agent): fixed completed indexes

* fix: Adding usize to completed_contexts_lock instead of &usize

* fix(game downloads): Added error handling for chunk request errors

* chore: Apply stashed changes

* feat(games): Added multi-argument game launch and setup support

* fix: Games not launching due to string semantics

* build: Version bump & appimage build

* chore: Update .gitlab-ci.yml

* Update .gitlab-ci.yml

* Update .gitlab-ci.yml with artifacts

* feat(settings): Made save button include user feedback & only allow numeric characters

* fix(library): Added "LIbrary Failed to Update" content to recover from library load fail

* fix(logging): Restored RUST_LOG env functionality

* Update changelog.md

---------

Signed-off-by: quexeky <git@quexeky.dev>
Signed-off-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Co-authored-by: seethruhead <shane.keulen@gmail.com>
2025-01-25 18:49:54 +11:00
26 changed files with 284 additions and 1232 deletions

View file

@ -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 === currentNavigation navIdx === currentPageIndex
? '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 {currentNavigation} = useCurrentNavigationIndex(navigation); const currentPageIndex = useCurrentNavigationIndex(navigation);
const quickActions: Array<QuickActionNav> = [ const quickActions: Array<QuickActionNav> = [
{ {

View file

@ -26,7 +26,5 @@ export const useCurrentNavigationIndex = (
currentNavigation.value = calculateCurrentNavIndex(to); currentNavigation.value = calculateCurrentNavIndex(to);
}); });
return {currentNavigation, recalculateNavigation: () => { return currentNavigation;
currentNavigation.value = calculateCurrentNavIndex(route);
}};
}; };

View file

@ -1,17 +1,28 @@
<template> <template>
<div class="flex flex-row h-full"> <div class="flex flex-row h-full">
<div class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"> <div
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 v-for="(nav, navIdx) in navigation" :key="nav.route" :class="[ <NuxtLink
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3', v-for="(nav, navIdx) in navigation"
navIdx === currentNavigation :key="nav.route"
? 'bg-zinc-800 text-zinc-100' :class="[
: nav.isInstalled.value 'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
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 class="size-6 flex-none object-cover bg-zinc-900 rounded" :src="icons[nav.id]" alt="" /> <img
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>
@ -20,47 +31,34 @@
</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 { listen } from "@tauri-apps/api/event"; import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
import { GameStatusEnum, type Game, type GameStatus, type NavigationItem } from "~/types";
let libraryDownloadError = false; let libraryDownloadError = false;
const games: { [key: string]: { game: Game, status: Ref<GameStatus, GameStatus> } } = {}; async function calculateGames(): Promise<Game[]> {
const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([])
async function calculateGames() {
try { try {
rawGames.value = await invoke("fetch_library"); return 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();
} }
} }
await calculateGames(); const rawGames: Array<Game> = await calculateGames();
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
const navigation = computed(() => rawGames.value.map((game) => { const icons = await Promise.all(
const status = games[game.id].status; games.map(({ game, status }) => useObject(game.mIconId))
);
const navigation = games.map(({ game, status }) => {
const isInstalled = computed( const isInstalled = computed(
() => () =>
status.value.type == GameStatusEnum.Installed || status.value.type == GameStatusEnum.Installed ||
@ -72,18 +70,9 @@ const navigation = computed(() => rawGames.value.map((game) => {
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>

View file

@ -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 === currentNavigation itemIdx === currentPageIndex
? '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 === currentNavigation itemIdx === currentPageIndex
? '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 {currentNavigation} = useCurrentNavigationIndex(navigation.value); const currentPageIndex = useCurrentNavigationIndex(navigation.value);
// Watch for navigation changes and update currentPageIndex // Watch for navigation changes and update currentPageIndex
watch(navigation, (newNav) => { watch(navigation, (newNav) => {
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value; currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
}); });
</script> </script>

View file

@ -1,7 +1,9 @@
<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 class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"> <div
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
@ -13,17 +15,27 @@
</p> </p>
</div> </div>
<div class="ml-4 mt-2 shrink-0"> <div class="ml-4 mt-2 shrink-0">
<button @click="() => (open = true)" type="button" <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"> @click="() => (open = true)"
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 v-for="(dir, dirIdx) in dirs" :key="dir" class="flex justify-between gap-x-6 py-5"> <li
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 class="h-6 w-6 text-blue-600 flex-none rounded-full" alt="" /> <FolderIcon
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 }}
@ -31,12 +43,16 @@
</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 @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[ <button
dirs.length <= 1 @click="() => deleteDirectory(dirIdx)"
? 'text-zinc-700' :disabled="dirs.length <= 1"
: 'text-zinc-400 hover:text-zinc-100', :class="[
'-m-2.5 block p-2.5', dirs.length <= 1
]"> ? '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>
@ -56,44 +72,37 @@
Maximum Download Threads Maximum Download Threads
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads" <input
@keypress="validateNumberInput" @paste="validatePaste" type="number"
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" /> name="threads"
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 type="button" @click="saveSettings" :disabled="saveState.loading" :class="[ <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', type="button"
saveState.success @click="saveDownloadThreads"
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600' :disabled="saveState.loading"
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600', :class="[
'disabled:bg-blue-600/50 disabled:cursor-not-allowed' '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',
]"> 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>
@ -101,27 +110,49 @@
</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 as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" <TransitionChild
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0"> as="template"
<div class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity" /> enter="ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity"
/>
</TransitionChild> </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 class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <div
<TransitionChild as="template" enter="ease-out duration-300" 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"
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" leave="ease-in duration-200" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-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 for="dir" class="block text-sm/6 font-medium text-zinc-100">Select game directory</label> <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 @click="() => selectDirectory()" <button
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"> @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"
>
{{ {{
currentDirectory ?? "Click to select a directory..." currentDirectory ?? "Click to select a directory..."
}} }}
@ -134,25 +165,36 @@
</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 :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading" <LoadingButton
@click="() => submitDirectory()" :class="[ :disabled="currentDirectory == undefined"
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 type="button" <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()" ref="cancelButtonRef"> @click="() => cancelDirectory()"
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 class="h-5 w-5 text-red-600" aria-hidden="true" /> <XCircleIcon
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">
@ -178,7 +220,6 @@ 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);
@ -190,7 +231,6 @@ 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,
@ -253,21 +293,21 @@ async function deleteDirectory(index: number) {
await updateDirs(); await updateDirs();
} }
async function saveSettings() { async function saveDownloadThreads() {
try { try {
saveState.loading = true; saveState.loading = true;
await invoke("update_settings", { await invoke("update_settings", {
newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value }, newSettings: { maxDownloadThreads: downloadThreads.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 {
@ -277,8 +317,8 @@ async function saveSettings() {
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

File diff suppressed because it is too large Load diff

View file

@ -50,11 +50,6 @@ 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"

View file

@ -74,8 +74,7 @@ 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;
drop(db_lock); println!("new Settings: {:?}", db_lock.settings);
save_db();
} }
#[tauri::command] #[tauri::command]
pub fn fetch_settings() -> Settings { pub fn fetch_settings() -> Settings {

View file

@ -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, Deserialize)] #[derive(Clone, Serialize)]
pub enum ApplicationTransientStatus { pub enum ApplicationTransientStatus {
Downloading { version_name: String }, Downloading { version_name: String },
Uninstalling {}, Uninstalling {},
@ -98,10 +98,9 @@ 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>, cache_dir: PathBuf) -> Self { fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
Self { Self {
applications: DatabaseApplications { applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()], install_dirs: vec![games_base_dir.into()],
@ -113,8 +112,10 @@ impl Database {
prev_database, prev_database,
base_url: "".to_owned(), base_url: "".to_owned(),
auth: None, auth: None,
settings: Settings::default(), settings: Settings {
cache_dir, autostart: false,
max_download_threads: 4,
},
} }
} }
} }
@ -149,23 +150,21 @@ 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).unwrap(); create_dir_all(games_base_dir.clone()).unwrap();
create_dir_all(&logs_root_dir).unwrap(); create_dir_all(logs_root_dir.clone()).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, cache_dir), Err(e) => handle_invalid_database(e, db_path, games_base_dir),
}, },
false => { false => {
let default = Database::new(games_base_dir, None, cache_dir); let default = Database::new(games_base_dir, None);
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()
@ -205,7 +204,6 @@ 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();
@ -222,7 +220,6 @@ 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")

View file

@ -5,7 +5,6 @@ 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 {
@ -13,7 +12,6 @@ impl Default for Settings {
Self { Self {
autostart: false, autostart: false,
max_download_threads: 4, max_download_threads: 4,
force_offline: false
} }
} }
} }

View file

@ -48,7 +48,7 @@ pub enum DownloadManagerSignal {
Uninstall(DownloadableMetadata), Uninstall(DownloadableMetadata),
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum DownloadManagerStatus { pub enum DownloadManagerStatus {
Downloading, Downloading,
Paused, Paused,

View file

@ -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); download_agent.on_error(&app_handle, e.clone());
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); current_agent.on_error(&self.app_handle, error.clone());
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata()); self.remove_and_cleanup_front_download(&current_agent.metadata());

View file

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

View file

@ -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, SerializeDisplay)] #[derive(Debug, Clone, SerializeDisplay)]
pub enum ApplicationDownloadError { pub enum ApplicationDownloadError {
Communication(RemoteAccessError), Communication(RemoteAccessError),
Checksum, Checksum,

View file

@ -10,7 +10,7 @@ use url::ParseError;
use super::drop_server_error::DropServerError; use super::drop_server_error::DropServerError;
#[derive(Debug, SerializeDisplay)] #[derive(Debug, Clone, SerializeDisplay)]
pub enum RemoteAccessError { pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>), FetchError(Arc<reqwest::Error>),
ParsingError(ParseError), ParsingError(ParseError),
@ -21,7 +21,6 @@ pub enum RemoteAccessError {
InvalidRedirect, InvalidRedirect,
ManifestDownloadFailed(StatusCode, String), ManifestDownloadFailed(StatusCode, String),
OutOfSync, OutOfSync,
Cache(cacache::Error),
Generic(String), Generic(String),
} }
@ -53,7 +52,6 @@ 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),
} }
} }
} }

View file

@ -1,9 +1,9 @@
use std::sync::Mutex; use std::sync::Mutex;
use tauri::{AppHandle, Manager}; use tauri::AppHandle;
use crate::{ use crate::{
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 database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
}; };
use super::{ use super::{
@ -15,16 +15,16 @@ use super::{
}; };
#[tauri::command] #[tauri::command]
pub fn fetch_library(state: tauri::State<'_, Mutex<AppState>>) -> Result<Vec<Game>, RemoteAccessError> { pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
offline!(state, fetch_library_logic, fetch_library_logic_offline, state) fetch_library_logic(app)
} }
#[tauri::command] #[tauri::command]
pub fn fetch_game( pub fn fetch_game(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>> app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
offline!(state, fetch_game_logic, fetch_game_logic_offline, game_id, state) fetch_game_logic(game_id, app)
} }
#[tauri::command] #[tauri::command]
@ -38,6 +38,7 @@ 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(())

View file

@ -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(ApplicationDownloadError::Communication)? .map_err(|e| ApplicationDownloadError::Communication(e))?
.send() .send()
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?; .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
@ -280,13 +280,9 @@ impl GameDownloadAgent {
) { ) {
Ok(request) => request, Ok(request) => request,
Err(e) => { Err(e) => {
sender sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
.send(DownloadManagerSignal::Error(
ApplicationDownloadError::Communication(e),
))
.unwrap();
continue; continue;
} },
}; };
scope.spawn(move |_| { scope.spawn(move |_| {
@ -366,7 +362,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())

View file

@ -14,17 +14,16 @@ 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(Serialize, Deserialize)] #[derive(serde::Serialize)]
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Game { pub struct Game {
id: String, id: String,
@ -67,9 +66,7 @@ pub struct StatsUpdateEvent {
pub time: usize, pub time: usize,
} }
pub fn fetch_library_logic( pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
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();
@ -86,6 +83,7 @@ pub fn fetch_library_logic(
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();
@ -99,32 +97,17 @@ pub fn fetch_library_logic(
.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,
state: tauri::State<'_, Mutex<AppState>>, app: tauri::AppHandle,
) -> 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);
@ -136,8 +119,6 @@ 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();
@ -174,18 +155,9 @@ 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>>,
@ -221,7 +193,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) {
debug!("triggered uninstall for agent"); println!("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
@ -241,7 +213,6 @@ 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,
@ -258,7 +229,6 @@ 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();
@ -269,10 +239,6 @@ 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
@ -282,7 +248,6 @@ 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,

View file

@ -40,13 +40,11 @@ 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, recieve_handshake}; use remote::auth::{self, generate_authorization_header, recieve_handshake};
use remote::commands::{ use remote::commands::{
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote auth_initiate, 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;
@ -62,10 +60,9 @@ 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, Eq, PartialEq)] #[derive(Clone, Copy, Serialize)]
pub enum AppStatus { pub enum AppStatus {
NotConfigured, NotConfigured,
Offline,
ServerError, ServerError,
SignedOut, SignedOut,
SignedIn, SignedIn,
@ -239,7 +236,6 @@ 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,
@ -331,7 +327,7 @@ pub fn run() {
} }
_ => { _ => {
warn!("menu event not handled: {:?}", event.id); println!("menu event not handled: {:?}", event.id);
} }
}) })
.build(app) .build(app)
@ -360,9 +356,35 @@ 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| {
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state(); // Drop leading /
offline!(state, fetch_object, fetch_object_offline, request, responder); 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() {
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 {

View file

@ -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> {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override); println!("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()

View file

@ -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::Offline, None), Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
Err(_) => return (AppStatus::SignedInNeedsReauth, None), Err(_) => return (AppStatus::SignedInNeedsReauth, None),
}; };
return (AppStatus::SignedIn, Some(user_result)); return (AppStatus::SignedIn, Some(user_result));

View file

@ -1,63 +0,0 @@
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()
}
}

View file

@ -1,16 +1,17 @@
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}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, AppState, AppStatus database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
}; };
use super::{ use super::{
auth::{auth_initiate_logic, recieve_handshake, setup}, cache::{cache_object, get_cached_object}, remote::use_remote_logic auth::{auth_initiate_logic, recieve_handshake, setup},
remote::use_remote_logic,
}; };
#[tauri::command] #[tauri::command]
@ -34,28 +35,6 @@ 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

View file

@ -1,55 +0,0 @@
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),
}
}

View file

@ -1,7 +1,4 @@
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;

View file

@ -38,7 +38,6 @@ 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",
@ -77,5 +76,4 @@ export type DownloadableMetadata = {
export type Settings = { export type Settings = {
autostart: boolean, autostart: boolean,
maxDownloadThreads: number, maxDownloadThreads: number,
forceOffline: boolean
} }