Compare commits

..

6 Commits

Author SHA1 Message Date
067161ee0a chore(version): Version bump
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-25 08:40:20 +10:00
7cbed9dc86 feat(workflow): Cherry-picked release.yml from develop 2025-05-25 08:35:21 +10:00
9fab0af4e8 fix: Removed unnecessary nightly feature try_trait_v2 2025-01-28 22:48:26 +11:00
3968a61786 fix(downloads): Fix rearranging download queue throwing error 2025-01-25 23:20:31 +11:00
8861fe4e3d chore: Version bump 2025-01-25 19:46:58 +11:00
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 085cd9481d
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 86f2fb19bd
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 ece11e7581
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 7ea8a24fdc
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:38 2024 +1030

    Add files via upload

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

    Delete src-tauri/Cargo.toml

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

    Add files via upload

commit 2eea7b97a8
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 9a635a10d1
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:14:49 2024 +1030

    Add files via upload

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

    Add files via upload

commit ea1be4d750
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
66 changed files with 819 additions and 4329 deletions

68
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: 'publish'
on:
workflow_dispatch: {}
release:
types: [published]
# This can be used to automatically publish nightlies at UTC nighttime
# schedule:
# - cron: "0 2 * * *" # run at 2 AM UTC
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest' # for Arm based macs (M1 and above).
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel based macs.
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: install frontend dependencies
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: dev-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: 'Auto-release v__VERSION__'
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "drop-base"]
path = drop-base
url = https://github.com/drop-oss/drop-base

View File

@ -1,31 +0,0 @@
<template>
<div>
<label for="launch" class="block text-sm/6 font-medium text-zinc-100"
>Launch string template</label
>
<div class="mt-2">
<input
type="text"
name="launch"
id="launch"
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-zinc-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
placeholder="{}"
aria-describedby="launch-description"
v-model="model!!.launchString"
/>
</div>
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
Override the launch string. Passed to system's default shell, and replaces
"{}" with the command to start the game.
<span class="font-semibold text-zinc-200"
>Leaving it blank will cause the game not to start.</span
>
</p>
</div>
</template>
<script setup lang="ts">
import type { FrontendGameConfiguration } from "~/composables/game";
const model = defineModel<FrontendGameConfiguration>();
</script>

View File

@ -1,122 +0,0 @@
<template>
<ModalTemplate size-class="max-w-4xl" v-model="open">
<template #default>
<div class="flex flex-row gap-x-4">
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
<ul role="list" class="-mx-2 space-y-1">
<li v-for="(tab, tabIdx) in tabs" :key="tab.name">
<button
@click="() => (currentTabIndex = tabIdx)"
:class="[
tabIdx == currentTabIndex
? 'bg-zinc-800 text-zinc-100'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100',
'transition w-full group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
]"
>
<component
:is="tab.icon"
:class="[
tabIdx == currentTabIndex
? 'text-zinc-100'
: 'text-gray-400 group-hover:text-zinc-100',
'size-6 shrink-0',
]"
aria-hidden="true"
/>
{{ tab.name }}
</button>
</li>
</ul>
</nav>
<div class="border-l-2 border-zinc-800 w-full grow pl-4">
<component
v-model="configuration"
:is="tabs[currentTabIndex]?.page"
/>
</div>
</div>
<div v-if="saveError" class="mt-5 rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ saveError }}
</h3>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
@click="() => save()"
:loading="saveLoading"
type="submit"
class="ml-2 w-full sm:w-fit"
>
Save
</LoadingButton>
<button
@click="() => (open = false)"
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>
<script setup lang="ts">
import type { Component } from "vue";
import {
RocketLaunchIcon,
ServerIcon,
TrashIcon,
XCircleIcon,
} from "@heroicons/vue/20/solid";
import Launch from "./GameOptions/Launch.vue";
import type { FrontendGameConfiguration } from "~/composables/game";
import { invoke } from "@tauri-apps/api/core";
const open = defineModel<boolean>();
const props = defineProps<{ gameId: string }>();
const game = await useGame(props.gameId);
const configuration: Ref<FrontendGameConfiguration> = ref({
launchString: game.version!!.launchCommandTemplate,
});
const tabs: Array<{ name: string; icon: Component; page: Component }> = [
{
name: "Launch",
icon: RocketLaunchIcon,
page: Launch,
},
{
name: "Storage",
icon: ServerIcon,
page: h("div"),
},
];
const currentTabIndex = ref(0);
const saveLoading = ref(false);
const saveError = ref<undefined | string>();
async function save() {
saveLoading.value = true;
try {
await invoke("update_game_configuration", {
gameId: game.game.id,
options: configuration.value,
});
open.value = false;
} catch (e) {
saveError.value = (e as unknown as string).toString();
}
saveLoading.value = false;
}
</script>

View File

@ -1,76 +1,32 @@
<template> <template>
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<div class="inline-flex divide-x divide-zinc-900"> <div class="inline-flex divide-x divide-zinc-900">
<button <button type="button" @click="() => buttonActions[props.status.type]()" :class="[
type="button"
@click="() => buttonActions[props.status.type]()"
:class="[
styles[props.status.type], styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md', showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', 'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]" ]">
> <component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }} {{ buttonNames[props.status.type] }}
</button> </button>
<Menu <Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full"> <div class="h-full">
<MenuButton <MenuButton :class="[
:class="[
styles[props.status.type], styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group', 'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm'
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', ]">
]"
>
<ChevronDownIcon class="size-5" aria-hidden="true" /> <ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton> </MenuButton>
</div> </div>
<transition <transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-active-class="transition ease-out duration-100" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
enter-from-class="transform opacity-0 scale-95" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems <MenuItems
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none" class="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
>
<div class="py-1"> <div class="py-1">
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('uninstall')"
@click="() => emit('options')" :class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']">Uninstall
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem>
<MenuItem v-slot="{ active }">
<button
@click="() => emit('uninstall')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Uninstall
<TrashIcon class="size-5" /> <TrashIcon class="size-5" />
</button> </button>
</MenuItem> </MenuItem>
@ -87,13 +43,13 @@ import {
ChevronDownIcon, ChevronDownIcon,
PlayIcon, PlayIcon,
QueueListIcon, QueueListIcon,
TrashIcon,
WrenchIcon, WrenchIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import type { Component } from "vue"; import type { Component } from "vue";
import { GameStatusEnum, type GameStatus } from "~/types.js"; import { GameStatusEnum, type GameStatus } from "~/types.js";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue"; import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import { Cog6ToothIcon, TrashIcon } from "@heroicons/vue/24/outline";
const props = defineProps<{ status: GameStatus }>(); const props = defineProps<{ status: GameStatus }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -102,32 +58,19 @@ const emit = defineEmits<{
(e: "queue"): void; (e: "queue"): void;
(e: "uninstall"): void; (e: "uninstall"): void;
(e: "kill"): void; (e: "kill"): void;
(e: "options"): void;
}>(); }>();
const showDropdown = computed( const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired);
() =>
props.status.type === GameStatusEnum.Installed ||
props.status.type === GameStatusEnum.SetupRequired
);
const styles: { [key in GameStatusEnum]: string } = { const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: [GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500", [GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Queued]: [GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", [GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
[GameStatusEnum.Downloading]: [GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", [GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.SetupRequired]: [GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500", [GameStatusEnum.Running]: "bg-zinc-800 text-white focus-visible:outline-zinc-700"
[GameStatusEnum.Installed]:
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500",
[GameStatusEnum.Updating]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Uninstalling]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
}; };
const buttonNames: { [key in GameStatusEnum]: string } = { const buttonNames: { [key in GameStatusEnum]: string } = {
@ -138,7 +81,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "Play", [GameStatusEnum.Installed]: "Play",
[GameStatusEnum.Updating]: "Updating", [GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling", [GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop", [GameStatusEnum.Running]: "Stop"
}; };
const buttonIcons: { [key in GameStatusEnum]: Component } = { const buttonIcons: { [key in GameStatusEnum]: Component } = {
@ -149,7 +92,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Installed]: PlayIcon, [GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon, [GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon, [GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon, [GameStatusEnum.Running]: PlayIcon
}; };
const buttonActions: { [key in GameStatusEnum]: () => void } = { const buttonActions: { [key in GameStatusEnum]: () => void } = {
@ -159,7 +102,7 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.SetupRequired]: () => emit("launch"), [GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"), [GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"), [GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {}, [GameStatusEnum.Uninstalling]: () => { },
[GameStatusEnum.Running]: () => emit("kill"), [GameStatusEnum.Running]: () => emit("kill")
}; };
</script> </script>

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',
]" ]"
@ -28,7 +28,9 @@
/> />
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<ol class="inline-flex gap-3"> <ol class="inline-flex gap-3">
<HeaderQueueWidget :object="currentQueueObject" /> <HeaderQueueWidget
:object="currentQueueObject"
/>
<li v-for="(item, itemIdx) in quickActions"> <li v-for="(item, itemIdx) in quickActions">
<HeaderWidget <HeaderWidget
@click="item.action" @click="item.action"
@ -37,7 +39,6 @@
<component class="h-5" :is="item.icon" /> <component class="h-5" :is="item.icon" />
</HeaderWidget> </HeaderWidget>
</li> </li>
<OfflineHeaderWidget v-if="state.status === AppStatus.Offline" />
<HeaderUserWidget /> <HeaderUserWidget />
</ol> </ol>
</div> </div>
@ -48,12 +49,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid"; import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid";
import { AppStatus, type NavigationItem, type QuickActionNav } from "../types"; import type { NavigationItem, QuickActionNav } from "../types";
import HeaderWidget from "./HeaderWidget.vue"; import HeaderWidget from "./HeaderWidget.vue";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
const window = getCurrentWindow(); const window = getCurrentWindow();
const state = useAppState();
const navigation: Array<NavigationItem> = [ const navigation: Array<NavigationItem> = [
{ {
@ -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

@ -49,10 +49,7 @@
Admin Dashboard Admin Dashboard
</a> </a>
</MenuItem> </MenuItem>
<MenuItem <MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active, close }">
v-for="(nav, navIdx) in navigation"
v-slot="{ active, close }"
>
<button <button
@click="() => navigate(close, nav)" @click="() => navigate(close, nav)"
:href="nav.route" :href="nav.route"
@ -61,8 +58,8 @@
'transition text-left block px-4 py-2 text-sm', 'transition text-left block px-4 py-2 text-sm',
]" ]"
> >
{{ nav.label }} {{ nav.label }}</button
</button> >
</MenuItem> </MenuItem>
</div> </div>
</PanelWidget> </PanelWidget>
@ -83,17 +80,17 @@ const open = ref(false);
const router = useRouter(); const router = useRouter();
router.afterEach(() => { router.afterEach(() => {
open.value = false; open.value = false;
}); })
const state = useAppState(); const state = useAppState();
const profilePictureUrl: string = await useObject( const profilePictureUrl: string = await invoke("gen_drop_url", {
state.value.user?.profilePicture ?? "" path: `/api/v1/object/${state.value.user?.profilePicture}`,
); });
const adminUrl: string = await invoke("gen_drop_url", { const adminUrl: string = await invoke("gen_drop_url", {
path: "/admin", path: "/admin",
}); });
function navigate(close: () => any, to: NavigationItem) { function navigate(close: () => any, to: NavigationItem){
close(); close();
router.push(to.route); router.push(to.route);
} }
@ -113,6 +110,6 @@ const navigation: NavigationItem[] = [
label: "Quit Drop", label: "Quit Drop",
route: "/quit", route: "/quit",
prefix: "", prefix: "",
}, }
]; ]
</script> </script>

View File

@ -126,7 +126,6 @@ import { invoke } from "@tauri-apps/api/core";
const loading = ref(false); const loading = ref(false);
const error = ref<string | undefined>(); const error = ref<string | undefined>();
let offerManualTimeout: NodeJS.Timeout | undefined;
const offerManual = ref(false); const offerManual = ref(false);
const manualToken = ref(""); const manualToken = ref("");
const manualLoading = ref(false); const manualLoading = ref(false);
@ -140,9 +139,8 @@ function authWrapper_wrapper() {
auth().catch((e) => { auth().catch((e) => {
loading.value = false; loading.value = false;
error.value = e; error.value = e;
if(offerManualTimeout) clearTimeout(offerManualTimeout);
}); });
offerManualTimeout = setTimeout(() => { setTimeout(() => {
offerManual.value = true; offerManual.value = true;
}, 10000); }, 10000);
} }

View File

@ -1,177 +0,0 @@
<template>
<div>
<div
class="relative mb-3 transition-transform duration-300 hover:scale-105 active:scale-95"
>
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..."
/>
</div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<NuxtLink
v-for="nav in filteredNavigation"
:key="nav.id"
:class="[
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
nav.index === currentNavigation
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="nav.route"
>
<div class="flex items-center w-full gap-x-3">
<div
class="flex-none transition-transform duration-300 hover:-rotate-2"
>
<img
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
:src="icons[nav.id]"
alt=""
/>
</div>
<div class="flex flex-col flex-1">
<p
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
>
{{ nav.label }}
</p>
<p
class="text-xs font-medium"
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
>
{{ gameStatusText[games[nav.id].status.value.type] }}
</p>
</div>
</div>
</NuxtLink>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
import { TransitionGroup } from "vue";
import { listen } from "@tauri-apps/api/event";
// Style information
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "text-green-500",
[GameStatusEnum.Downloading]: "text-blue-500",
[GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-500",
[GameStatusEnum.Queued]: "text-blue-500",
[GameStatusEnum.Updating]: "text-blue-500",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading...",
[GameStatusEnum.Installed]: "Installed",
[GameStatusEnum.Updating]: "Updating...",
[GameStatusEnum.Uninstalling]: "Uninstalling...",
[GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running",
};
const router = useRouter();
const searchQuery = ref("");
const games: {
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
} = {};
const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([]);
async function calculateGames() {
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);
}
}
await calculateGames();
const navigation = computed(() =>
rawGames.value.map((game) => {
const status = games[game.id].status;
const isInstalled = computed(
() =>
status.value.type == GameStatusEnum.Installed ||
status.value.type == GameStatusEnum.SetupRequired
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
id: game.id,
};
return item;
})
);
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
navigation.value
);
const filteredNavigation = computed(() => {
if (!searchQuery.value)
return navigation.value.map((e, i) => ({ ...e, index: i }));
const query = searchQuery.value.toLowerCase();
return navigation.value
.filter((nav) => nav.label.toLowerCase().includes(query))
.map((e, i) => ({ ...e, index: i }));
});
listen("update_library", async (event) => {
console.log("Updating library");
let oldNavigation = navigation.value[currentNavigation.value];
await calculateGames();
recalculateNavigation();
if (oldNavigation !== navigation.value[currentNavigation.value]) {
console.log("Triggered");
router.push("/library");
}
});
</script>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-active {
position: absolute;
}
</style>

View File

@ -1,17 +0,0 @@
<script setup lang="ts">
import { ArrowDownTrayIcon, CloudIcon } from "@heroicons/vue/20/solid";
</script>
<template>
<div
class="transition inline-flex items-center rounded-sm px-4 py-1.5 bg-zinc-900 text-sm text-zinc-400 gap-x-2"
>
<div class="relative">
<CloudIcon class="h-5 z-50 text-zinc-500" />
<div
class="absolute rounded-full left-1/2 top-1/2 -translate-y-[45%] -translate-x-1/2 w-[2px] h-6 rotate-[45deg] bg-zinc-400 z-50"
/>
</div>
Offline
</div>
</template>

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,9 +1,8 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types"; import type { Game, GameStatus, GameStatusEnum } from "~/types";
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } = const gameRegistry: { [key: string]: Game } = {};
{};
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {}; const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
@ -32,44 +31,27 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
export const useGame = async (gameId: string) => { export const useGame = async (gameId: string) => {
if (!gameRegistry[gameId]) { if (!gameRegistry[gameId]) {
const data: { const data: { game: Game; status: SerializedGameStatus } = await invoke(
game: Game; "fetch_game",
status: SerializedGameStatus; {
version?: GameVersion;
} = await invoke("fetch_game", {
gameId, gameId,
}); }
gameRegistry[gameId] = { game: data.game, version: data.version }; );
gameRegistry[gameId] = data.game;
if (!gameStatusRegistry[gameId]) { if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[gameId] = ref(parseStatus(data.status)); gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => { listen(`update_game/${gameId}`, (event) => {
const payload: { const payload: {
status: SerializedGameStatus; status: SerializedGameStatus;
version?: GameVersion;
} = event.payload as any; } = event.payload as any;
console.log(payload.status); console.log(payload.status);
gameStatusRegistry[gameId].value = parseStatus(payload.status); gameStatusRegistry[gameId].value = parseStatus(payload.status);
/**
* I am not super happy about this.
*
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
* on transient state updates.
*/
if (payload.version) {
gameRegistry[gameId].version = payload.version;
}
}); });
} }
} }
const game = gameRegistry[gameId]; const game = gameRegistry[gameId];
const status = gameStatusRegistry[gameId]; const status = gameStatusRegistry[gameId];
return { ...game, status }; return { game, status };
};
export type FrontendGameConfiguration = {
launchString: string;
}; };

Submodule drop-base deleted from 26698e5b06

View File

@ -1,90 +0,0 @@
<template>
<NuxtLayout name="default">
<div
class="grid min-h-full grid-cols-1 grid-rows-[1fr,auto,1fr] lg:grid-cols-[max(50%,36rem),1fr]"
>
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<Logo class="h-10 w-auto sm:h-12" />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
>
<div class="max-w-lg">
<p class="text-base font-semibold leading-8 text-blue-600">
{{ error?.statusCode }}
</p>
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Oh no!
</h1>
<p
v-if="message"
class="mt-3 font-bold text-base leading-7 text-red-500"
>
{{ message }}
</p>
<p class="mt-6 text-base leading-7 text-zinc-400">
An error occurred while responding to your request. If you believe
this to be a bug, please report it. Try signing in and see if it
resolves the issue.
</p>
<div class="mt-10">
<!-- full app reload to fix errors -->
<a
href="/store"
class="text-sm font-semibold leading-7 text-blue-600"
><span aria-hidden="true">&larr;</span> Back to store</a
>
</div>
</div>
</main>
<footer class="self-end lg:col-span-2 lg:col-start-1 lg:row-start-3">
<div class="border-t border-zinc-700 bg-zinc-900 py-10">
<nav
class="mx-auto flex w-full max-w-7xl items-center gap-x-4 px-6 text-sm leading-7 text-zinc-400 lg:px-8"
>
<NuxtLink href="/docs">Documentation</NuxtLink>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-600"
>
<circle cx="1" cy="1" r="1" />
</svg>
<a href="https://discord.gg/NHx46XKJWA" target="_blank"
>Support Discord</a
>
</nav>
</div>
</footer>
<div
class="hidden lg:relative lg:col-start-2 lg:row-start-1 lg:row-end-4 lg:block"
>
<img
src="@/assets/wallpaper.jpg"
alt=""
class="absolute inset-0 h-full w-full object-cover"
/>
</div>
</div>
</NuxtLayout>
</template>
<script setup lang="ts">
import type { NuxtError } from "#app";
const props = defineProps({
error: Object as () => NuxtError,
});
const statusCode = props.error?.statusCode;
const message =
props.error?.statusMessage ||
props.error?.message ||
"An unknown error occurred.";
console.error(props.error);
</script>

View File

@ -1,79 +1,9 @@
<template> <template>
<div class="flex flex-col bg-zinc-900 overflow-hidden h-screen"> <div class="flex flex-col bg-zinc-900 overflow-hidden">
<NuxtErrorBoundary>
<Header class="select-none" /> <Header class="select-none" />
<div class="relative grow overflow-y-auto"> <div class="relative grow overflow-y-auto">
<slot /> <slot />
</div> </div>
<template #error="{ error }">
<MiniHeader />
<div class="relative grow overflow-y-auto bg-zinc-950">
<div
class="grid min-h-full grid-cols-1 grid-rows-[1fr,auto,1fr] lg:grid-cols-[max(50%,36rem),1fr]"
>
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<Logo class="h-10 w-auto sm:h-12" />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
>
<div class="max-w-lg">
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Unrecoverable error
</h1>
<p class="mt-6 text-base leading-7 text-zinc-400">
Drop encountered an error that it couldn't handle. Please
restart the application and file a bug report.
</p>
<p class="mt-3 text-sm font-monospace text-zinc-500">
Error: {{ error }}
</p>
</div>
</main>
<footer
class="self-end lg:col-span-2 lg:col-start-1 lg:row-start-3"
>
<div class="border-t border-blue-600 bg-zinc-900 py-10">
<nav
class="mx-auto flex w-full max-w-7xl items-center gap-x-4 px-6 text-sm leading-7 text-zinc-400 lg:px-8"
>
<a href="#">Documentation</a>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-700"
>
<circle cx="1" cy="1" r="1" />
</svg>
<a href="#">Troubleshooting</a>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-700"
>
<circle cx="1" cy="1" r="1" />
</svg>
<NuxtLink to="/setup/server">Switch instance</NuxtLink>
</nav>
</div>
</footer>
<div
class="hidden lg:relative lg:col-start-2 lg:row-start-1 lg:row-end-4 lg:block"
>
<img
src="@/assets/wallpaper.jpg"
alt=""
class="absolute inset-0 h-full w-full object-cover"
/>
</div>
</div>
</div>
</template>
</NuxtErrorBoundary>
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col bg-zinc-950 overflow-hidden h-screen"> <div class="flex flex-col bg-zinc-950 overflow-hidden">
<MiniHeader /> <MiniHeader />
<div class="relative grow overflow-y-auto"> <div class="relative grow overflow-y-auto">
<slot /> <slot />

View File

@ -13,5 +13,5 @@ export default defineNuxtConfig({
ssr: false, ssr: false,
extends: [["./drop-base"]], extends: [["github:drop-oss/drop-base"]],
}); });

View File

@ -19,11 +19,9 @@
"@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-os": "~2", "@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-shell": "^2.2.1", "@tauri-apps/plugin-shell": ">=2.0.0",
"koa": "^2.16.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"micromark": "^4.0.1", "nuxt": "^3.13.0",
"nuxt": "^3.16.0",
"scss": "^0.2.4", "scss": "^0.2.4",
"vue": "latest", "vue": "latest",
"vue-router": "latest", "vue-router": "latest",

66
pages/error/index.vue Normal file
View File

@ -0,0 +1,66 @@
<template>
<div
class="grid min-h-full grid-cols-1 grid-rows-[1fr,auto,1fr] lg:grid-cols-[max(50%,36rem),1fr]"
>
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<Logo class="h-10 w-auto sm:h-12" />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
>
<div class="max-w-lg">
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Unrecoverable error
</h1>
<p class="mt-6 text-base leading-7 text-zinc-400">
Drop encountered an error that it couldn't handle. Please restart the
application and file a bug report.
</p>
</div>
</main>
<footer class="self-end lg:col-span-2 lg:col-start-1 lg:row-start-3">
<div class="border-t border-blue-600 bg-zinc-900 py-10">
<nav
class="mx-auto flex w-full max-w-7xl items-center gap-x-4 px-6 text-sm leading-7 text-zinc-400 lg:px-8"
>
<a href="#">Documentation</a>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-700"
>
<circle cx="1" cy="1" r="1" />
</svg>
<a href="#">Troubleshooting</a>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-700"
>
<circle cx="1" cy="1" r="1" />
</svg>
<NuxtLink to="/setup/server">Switch instance</NuxtLink>
</nav>
</div>
</footer>
<div
class="hidden lg:relative lg:col-start-2 lg:row-start-1 lg:row-end-4 lg:block"
>
<img
src="@/assets/wallpaper.jpg"
alt=""
class="absolute inset-0 h-full w-full object-cover"
/>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: "mini",
});
</script>

View File

@ -1,52 +1,78 @@
<template> <template>
<div class="flex flex-row h-full"> <div class="flex flex-row h-full">
<!-- Sidebar -->
<div <div
class="flex-none max-h-full overflow-y-auto w-72 bg-zinc-950/50 backdrop-blur-xl px-4 py-3 border-r border-zinc-800/50" class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"
> >
<LibrarySearch /> <ul class="flex flex-col gap-y-1">
</div> <NuxtLink
<div class="grow overflow-y-auto"> v-for="(nav, navIdx) in navigation"
<NuxtErrorBoundary> :key="nav.route"
<NuxtPage /> :class="[
<template #error="{ error }"> 'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
<main navIdx === currentNavigationIndex
class="grid min-h-full w-full place-items-center px-6 py-24 sm:py-32 lg:px-8" ? 'bg-zinc-800 text-zinc-100'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="nav.route"
> >
<div class="text-center"> <div class="flex items-center w-full gap-x-3">
<p class="text-base font-semibold text-blue-600">Error</p> <img
<h1 class="size-6 flex-none object-cover bg-zinc-900 rounded"
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl" :src="icons[navIdx]"
> alt=""
Failed to load library />
</h1> <p class="truncate text-sm font-display leading-6 flex-1">
<p class="mt-6 text-base leading-7 text-zinc-400"> {{ nav.label }}
Drop couldn't load your library: "{{ error }}".
</p> </p>
</div> </div>
</main> </NuxtLink>
</template> </ul>
</NuxtErrorBoundary> </div>
<div class="grow overflow-y-auto">
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
<style scoped> let libraryDownloadError = false;
.list-move,
.list-enter-active, async function calculateGames(): Promise<Game[]> {
.list-leave-active { try {
transition: all 0.3s ease; return await invoke("fetch_library");
}
catch(e) {
libraryDownloadError = true;
return new Array();
}
} }
.list-enter-from, const rawGames: Array<Game> = await calculateGames();
.list-leave-to { const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
opacity: 0; const icons = await Promise.all(
transform: translateX(-30px); games.map(({ game, status }) => useObject(game.mIconId))
} );
.list-leave-active { const navigation = games.map(({ game, status }) => {
position: absolute; const isInstalled = computed(
} () =>
</style> status.value.type == GameStatusEnum.Installed ||
status.value.type == GameStatusEnum.SetupRequired
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
};
return item;
});
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
</script>

View File

@ -1,155 +1,43 @@
<template> <template>
<div <div
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden" class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden"
> >
<div class="absolute inset-0 z-0"> <!-- banner image -->
<img <div class="absolute flex top-0 h-fit inset-x-0 z-[-20]">
:src="bannerUrl" <img :src="bannerUrl" class="w-full h-auto object-cover" />
class="w-full h-[24rem] object-cover blur-sm scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-zinc-900/80 to-transparent opacity-90"
/>
<div
class="absolute inset-0 bg-gradient-to-r from-zinc-900/95 via-zinc-900/80 to-transparent opacity-90"
/>
</div>
<div class="relative z-10">
<div class="px-8 pb-4">
<h1 <h1
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8" class="absolute inset-x-0 w-fit mx-auto text-center top-32 -translate-y-[50%] text-4xl text-zinc-100 font-bold font-display z-50 p-4 shadow-xl bg-zinc-900/80 rounded-xl"
> >
{{ game.mName }} {{ game.mName }}
</h1> </h1>
<div
<div class="flex flex-row gap-x-4 items-stretch mb-8"> class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 --> />
</div>
<!-- main page -->
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<!-- game toolbar -->
<div class="h-full flex flex-row gap-x-4 items-stretch">
<GameStatusButton <GameStatusButton
@install="() => installFlow()" @install="() => installFlow()"
@launch="() => launch()" @launch="() => launch()"
@queue="() => queue()" @queue="() => queue()"
@uninstall="() => uninstall()" @uninstall="() => uninstall()"
@kill="() => kill()" @kill="() => kill()"
@options="() => (configureModalOpen = true)"
:status="status" :status="status"
/> />
<a <a
:href="remoteUrl" :href="remoteUrl"
target="_blank" target="_blank"
type="button" type="button"
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display" class="inline-flex items-center rounded-md bg-zinc-800/50 px-4 font-semibold text-white shadow-sm hover:bg-zinc-800/80 uppercase font-display"
> >
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" /> <BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store Store
</a> </a>
</div> </div>
</div> </div>
<!-- Main content -->
<div class="w-full bg-zinc-900 px-8 py-6">
<div class="grid grid-cols-[2fr,1fr] gap-8">
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<div
v-html="htmlDescription"
class="prose prose-invert prose-blue overflow-y-auto custom-scrollbar max-w-none"
></div>
</div>
</div>
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
Game Images
</h2>
<div class="relative">
<div v-if="mediaUrls.length > 0">
<div
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
>
<div
class="absolute inset-0"
@click="fullscreenImage = mediaUrls[currentImageIndex]"
>
<TransitionGroup name="slide" tag="div" class="h-full">
<img
v-for="(url, index) in mediaUrls"
:key="url"
:src="url"
class="absolute inset-0 w-full h-full object-cover"
v-show="index === currentImageIndex"
/>
</TransitionGroup>
</div>
<div
class="absolute inset-0 flex items-center justify-between px-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
>
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
@click.stop="previousImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
<ChevronLeftIcon class="size-5" />
</button>
</div>
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
@click.stop="nextImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
<ChevronRightIcon class="size-5" />
</button>
</div>
</div>
<div
class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
/>
<div
class="absolute bottom-4 right-4 flex items-center gap-x-2 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
>
<ArrowsPointingOutIcon class="size-5" />
<span class="text-sm font-medium">View Fullscreen</span>
</div>
</div>
<div
class="absolute -bottom-2 left-1/2 -translate-x-1/2 flex gap-x-2"
>
<button
v-for="(_, index) in mediaUrls"
:key="index"
@click.stop="currentImageIndex = index"
class="w-1.5 h-1.5 rounded-full transition-all"
:class="[
currentImageIndex === index
? 'bg-zinc-100 scale-125'
: 'bg-zinc-600 hover:bg-zinc-500',
]"
/>
</div>
</div>
<div
v-else
class="aspect-video rounded-lg overflow-hidden bg-zinc-900/50 flex flex-col items-center justify-center text-center px-4"
>
<PhotoIcon class="size-12 text-zinc-500 mb-2" />
<p class="text-zinc-400 font-medium">No images available</p>
<p class="text-zinc-500 text-sm">
Game screenshots will appear here when available
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<ModalTemplate v-model="installFlowOpen"> <ModalTemplate v-model="installFlowOpen">
@ -368,74 +256,6 @@
</button> </button>
</template> </template>
</ModalTemplate> </ModalTemplate>
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
<Transition
enter="transition ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="transition ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
v-if="fullscreenImage"
class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
@click="fullscreenImage = null"
>
<div
class="relative w-full h-full flex items-center justify-center"
@click.stop
>
<button
class="absolute top-4 right-4 p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
@click.stop="fullscreenImage = null"
>
<XMarkIcon class="size-6" />
</button>
<button
v-if="mediaUrls.length > 1"
@click.stop="previousImage()"
class="absolute left-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
<ChevronLeftIcon class="size-6" />
</button>
<button
v-if="mediaUrls.length > 1"
@click.stop="nextImage()"
class="absolute right-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
<ChevronRightIcon class="size-6" />
</button>
<TransitionGroup
name="slide"
tag="div"
class="w-full h-full flex items-center justify-center"
@click.stop
>
<img
v-for="(url, index) in mediaUrls"
v-show="currentImageIndex === index"
:key="url"
:src="url"
class="max-h-[90vh] max-w-[90vw] object-contain"
:alt="`${game.mName} screenshot ${index + 1}`"
/>
</TransitionGroup>
<div
class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-zinc-900/50 backdrop-blur-sm"
>
<p class="text-zinc-100 text-sm font-medium">
{{ currentImageIndex + 1 }} / {{ mediaUrls.length }}
</p>
</div>
</div>
</div>
</Transition>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -454,17 +274,10 @@ import {
CheckIcon, CheckIcon,
ChevronUpDownIcon, ChevronUpDownIcon,
WrenchIcon, WrenchIcon,
ChevronLeftIcon,
ChevronRightIcon,
XMarkIcon,
ArrowsPointingOutIcon,
PhotoIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline"; import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid"; import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { micromark } from "micromark";
import { GameStatusEnum } from "~/types";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -479,22 +292,11 @@ const remoteUrl: string = await invoke("gen_drop_url", {
const bannerUrl = await useObject(game.value.mBannerId); const bannerUrl = await useObject(game.value.mBannerId);
// Get all available images
const mediaUrls = await Promise.all(
game.value.mImageCarousel.map((id) => useObject(id))
);
const htmlDescription = micromark(game.value.mDescription);
const installFlowOpen = ref(false); const installFlowOpen = ref(false);
const versionOptions = ref< const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }> undefined | Array<{ versionName: string; platform: string }>
>(); >();
const installDirs = ref<undefined | Array<string>>(); const installDirs = ref<undefined | Array<string>>();
const currentImageIndex = ref(0);
const configureModalOpen = ref(false);
async function installFlow() { async function installFlow() {
installFlowOpen.value = true; installFlowOpen.value = true;
versionOptions.value = undefined; versionOptions.value = undefined;
@ -517,7 +319,8 @@ const installVersionIndex = ref(0);
const installDir = ref(0); const installDir = ref(0);
async function install() { async function install() {
try { try {
if (!versionOptions.value) throw new Error("Versions have not been loaded"); if (!versionOptions.value)
throw new Error("Versions have not been loaded");
installLoading.value = true; installLoading.value = true;
await invoke("download_game", { await invoke("download_game", {
gameId: game.value.id, gameId: game.value.id,
@ -573,61 +376,4 @@ async function kill() {
console.error(e); console.error(e);
} }
} }
function nextImage() {
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
}
function previousImage() {
currentImageIndex.value =
(currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
}
const fullscreenImage = ref<string | null>(null);
</script> </script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.slide-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgb(82 82 91) transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgb(82 82 91);
border-radius: 3px;
}
</style>

View File

@ -1,19 +1,9 @@
<template>
<div class="h-full flex flex-col items-center justify-center">
<div class="text-center">
<div class="flex flex-col items-center gap-y-4">
<div class="p-4 rounded-xl bg-zinc-700/50 backdrop-blur-sm">
<RocketLaunchIcon class="size-12 text-zinc-400" />
</div>
<div>
<h3 class="text-xl font-display font-semibold text-zinc-100">Select a game</h3>
<p class="mt-1 text-sm text-zinc-400">Choose a game from your library to view details</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { RocketLaunchIcon } from '@heroicons/vue/24/outline'; const props = defineProps<{ libraryDownloadError: boolean }>();
</script> </script>
<template>
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
Library Failed to update
</div>
</template>

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
@click="() => deleteDirectory(dirIdx)"
:disabled="dirs.length <= 1"
:class="[
dirs.length <= 1 dirs.length <= 1
? 'text-zinc-700' ? 'text-zinc-700'
: 'text-zinc-400 hover:text-zinc-100', : 'text-zinc-400 hover:text-zinc-100',
'-m-2.5 block p-2.5', '-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
type="button"
@click="saveDownloadThreads"
:disabled="saveState.loading"
:class="[
'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', '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 saveState.success
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600' ? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600', : 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
'disabled:bg-blue-600/50 disabled:cursor-not-allowed' '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,11 +293,11 @@ 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

View File

@ -1,4 +1,2 @@
<template> <template></template>
<iframe src="server://drop.local/store" class="w-full h-full" />
</template>
<script setup lang="ts"></script> <script setup lang="ts"></script>

View File

@ -1,11 +1,8 @@
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
// Also possible // Also possible
/*
nuxtApp.hook("vue:error", (error, instance, info) => { nuxtApp.hook("vue:error", (error, instance, info) => {
console.error(error, info); console.error(error, info);
const router = useRouter(); const router = useRouter();
router.replace(`/error`); router.replace(`/error`);
}); });
*/
}); });

1589
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,11 @@ rustflags = ["-C", "target-feature=+aes,+sse2"]
tauri-build = { version = "2.0.0", features = [] } tauri-build = { version = "2.0.0", features = [] }
[dependencies] [dependencies]
tauri-plugin-shell = "2.2.1" tauri-plugin-shell = "2.0.0"
serde_json = "1" serde_json = "1"
serde-binary = "0.5.0" serde-binary = "0.5.0"
rayon = "1.10.0" rayon = "1.10.0"
directories = "5.0.1"
webbrowser = "1.0.2" webbrowser = "1.0.2"
url = "2.5.2" url = "2.5.2"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2"
@ -49,35 +50,12 @@ 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"
deranged = "=0.4.0"
droplet-rs = "0.7.3"
gethostname = "1.0.1"
zstd = "0.13.3"
tar = "0.4.44"
rand = "0.9.1"
regex = "1.11.1"
tempfile = "3.19.1"
schemars = "0.8.22"
sha1 = "0.10.6"
dirs = "6.0.0"
whoami = "1.6.0"
filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
[dependencies.dynfmt]
version = "0.1.5"
features = ["curly"]
[dependencies.tauri] [dependencies.tauri]
version = "2.1.1" version = "2.1.1"
features = ["tray-icon"] features = ["tray-icon"]
[dependencies.tokio] [dependencies.tokio]
version = "1.40.0" version = "1.40.0"
features = ["rt", "tokio-macros", "signal"] features = ["rt", "tokio-macros", "signal"]
@ -92,7 +70,15 @@ features = ["fs"]
[dependencies.uuid] [dependencies.uuid]
version = "1.10.0" version = "1.10.0"
features = ["v4", "fast-rng", "macro-diagnostics"] features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]
[dependencies.openssl]
version = "0.10.66"
features = ["vendored"]
[dependencies.rustbreak] [dependencies.rustbreak]
version = "2" version = "2"

View File

@ -1,102 +0,0 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr};
use log::warn;
use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
use super::path::CommonPath;
pub struct BackupManager<'a> {
pub current_platform: Platform,
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
}
impl BackupManager<'_> {
pub fn new() -> Self {
BackupManager {
#[cfg(target_os = "windows")]
current_platform: Platform::Windows,
#[cfg(target_os = "macos")]
current_platform: Platform::MacOs,
#[cfg(target_os = "linux")]
current_platform: Platform::Linux,
sources: HashMap::from([
// Current platform to target platform
(
(Platform::Windows, Platform::Windows),
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::Linux, Platform::Linux),
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::MacOs, Platform::MacOs),
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
]),
}
}
}
pub trait BackupHandler: Send + Sync {
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
fn store_user_id_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError) }
fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&whoami::username()).unwrap()) }
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winAppData>"); Err(BackupError::InvalidSystem) }
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppData>"); Err(BackupError::InvalidSystem) }
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>"); Err(BackupError::InvalidSystem) }
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDocuments>"); Err(BackupError::InvalidSystem) }
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winPublic>"); Err(BackupError::InvalidSystem) }
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winProgramData>"); Err(BackupError::InvalidSystem) }
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDir>"); Err(BackupError::InvalidSystem) }
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgData>"); Err(BackupError::InvalidSystem) }
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgConfig>"); Err(BackupError::InvalidSystem) }
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::new()) }
}
pub struct LinuxBackupManager {}
impl BackupHandler for LinuxBackupManager {
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Data.get().ok_or(BackupError::NotFound)?)
}
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
}
}
pub struct WindowsBackupManager {}
impl BackupHandler for WindowsBackupManager {
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
}
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::DataLocal.get().ok_or(BackupError::NotFound)?)
}
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::DataLocalLow.get().ok_or(BackupError::NotFound)?)
}
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/Windows").unwrap())
}
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Document.get().ok_or(BackupError::NotFound)?)
}
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
}
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Public.get().ok_or(BackupError::NotFound)?)
}
}
pub struct MacBackupManager {}
impl BackupHandler for MacBackupManager {}

View File

@ -1,6 +0,0 @@
use crate::process::process_manager::Platform;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Condition {
Os(Platform)
}

View File

@ -1,35 +0,0 @@
use crate::database::db::GameVersion;
use super::conditions::{Condition};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CloudSaveMetadata {
pub files: Vec<GameFile>,
pub game_version: GameVersion,
pub save_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GameFile {
pub path: String,
pub id: Option<String>,
pub data_type: DataType,
pub tags: Vec<Tag>,
pub conditions: Vec<Condition>
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub enum DataType {
Registry,
File,
Other
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Tag {
Config,
Save,
#[default]
#[serde(other)]
Other,
}

View File

@ -1,7 +0,0 @@
pub mod conditions;
pub mod metadata;
pub mod resolver;
pub mod placeholder;
pub mod normalise;
pub mod path;
pub mod backup_manager;

View File

@ -1,162 +0,0 @@
use std::sync::LazyLock;
use regex::Regex;
use crate::process::process_manager::Platform;
use super::placeholder::*;
pub fn normalize(path: &str, os: Platform) -> String {
let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
if path == "~" || path.starts_with("~/") {
path = path.replacen('~', HOME, 1);
}
static CONSECUTIVE_SLASHES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
static UNNECESSARY_DOUBLE_STAR_1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
static UNNECESSARY_DOUBLE_STAR_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
static ENDING_WILDCARD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
static ENDING_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
static INTERMEDIATE_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
static BLANK_SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
static APP_DATA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
static APP_DATA_ROAMING: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
static APP_DATA_LOCAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
static APP_DATA_LOCAL_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
static USER_PROFILE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
static DOCUMENTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
for (pattern, replacement) in [
(&CONSECUTIVE_SLASHES, "/"),
(&UNNECESSARY_DOUBLE_STAR_1, "${1}*"),
(&UNNECESSARY_DOUBLE_STAR_2, "*${1}"),
(&ENDING_WILDCARD, ""),
(&ENDING_DOT, ""),
(&INTERMEDIATE_DOT, "/"),
(&BLANK_SEGMENT, "/"),
(&APP_DATA, WIN_APP_DATA),
(&APP_DATA_ROAMING, WIN_APP_DATA),
(&APP_DATA_LOCAL, WIN_LOCAL_APP_DATA),
(&APP_DATA_LOCAL_2, &format!("{}/", WIN_LOCAL_APP_DATA)),
(&USER_PROFILE, HOME),
(&DOCUMENTS, WIN_DOCUMENTS),
] {
path = pattern.replace_all(&path, replacement).to_string();
}
if os == Platform::Windows {
let documents_2: Regex = Regex::new(r"(?i)<home>/Documents").unwrap();
#[allow(clippy::single_element_loop)]
for (pattern, replacement) in [(&documents_2, WIN_DOCUMENTS)] {
path = pattern.replace_all(&path, replacement).to_string();
}
}
for (pattern, replacement) in [
("{64BitSteamID}", STORE_USER_ID),
("{Steam3AccountID}", STORE_USER_ID),
] {
path = path.replace(pattern, replacement);
}
path
}
fn too_broad(path: &str) -> bool {
println!("Path: {}", path);
use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA};
let path_lower = path.to_lowercase();
for item in ALL {
if path == *item {
return true;
}
}
for item in AVOID_WILDCARDS {
if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) {
return true;
}
}
// These paths are present whether or not the game is installed.
// If possible, they should be narrowed down on the wiki.
for item in [
format!("{}/{}", BASE, STORE_USER_ID), // because `<storeUserId>` is handled as `*`
format!("{}/Documents", HOME),
format!("{}/Saved Games", HOME),
format!("{}/AppData", HOME),
format!("{}/AppData/Local", HOME),
format!("{}/AppData/Local/Packages", HOME),
format!("{}/AppData/LocalLow", HOME),
format!("{}/AppData/Roaming", HOME),
format!("{}/Documents/My Games", HOME),
format!("{}/Library/Application Support", HOME),
format!("{}/Library/Application Support/UserData", HOME),
format!("{}/Library/Preferences", HOME),
format!("{}/.renpy", HOME),
format!("{}/.renpy/persistent", HOME),
format!("{}/Library", HOME),
format!("{}/Library/RenPy", HOME),
format!("{}/Telltale Games", HOME),
format!("{}/config", ROOT),
format!("{}/MMFApplications", WIN_APP_DATA),
format!("{}/RenPy", WIN_APP_DATA),
format!("{}/RenPy/persistent", WIN_APP_DATA),
format!("{}/win.ini", WIN_DIR),
format!("{}/SysWOW64", WIN_DIR),
format!("{}/My Games", WIN_DOCUMENTS),
format!("{}/Telltale Games", WIN_DOCUMENTS),
format!("{}/unity3d", XDG_CONFIG),
format!("{}/unity3d", XDG_DATA),
"C:/Program Files".to_string(),
"C:/Program Files (x86)".to_string(),
] {
let item = item.to_lowercase();
if path_lower == item
|| path_lower.starts_with(&format!("{}/*", item))
|| path_lower.starts_with(&format!("{}/{}", item, STORE_USER_ID.to_lowercase()))
|| path_lower.starts_with(&format!("{}/savesdir", item))
{
return true;
}
}
// Drive letters:
let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
if drives.is_match(path) {
return true;
}
// Colon not for a drive letter
if path.get(2..).is_some_and(|path| path.contains(':')) {
return true;
}
// Root:
if path == "/" {
return true;
}
// Relative path wildcard:
if path.starts_with('*') {
return true;
}
false
}
pub fn usable(path: &str) -> bool {
let unprintable: Regex = Regex::new(r"(\p{Cc}|\p{Cf})").unwrap();
!path.is_empty()
&& !path.contains("{{")
&& !path.starts_with("./")
&& !path.starts_with("../")
&& !too_broad(path)
&& !unprintable.is_match(path)
}

View File

@ -1,48 +0,0 @@
use std::{path::PathBuf, sync::LazyLock};
pub enum CommonPath {
Config,
Data,
DataLocal,
DataLocalLow,
Document,
Home,
Public,
SavedGames,
}
impl CommonPath {
pub fn get(&self) -> Option<PathBuf> {
static CONFIG: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::config_dir());
static DATA: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_dir());
static DATA_LOCAL: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_local_dir());
static DOCUMENT: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::document_dir());
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::home_dir());
static PUBLIC: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::public_dir());
#[cfg(windows)]
static DATA_LOCAL_LOW: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow)
});
#[cfg(not(windows))]
static DATA_LOCAL_LOW: Option<PathBuf> = None;
#[cfg(windows)]
static SAVED_GAMES: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
known_folders::get_known_folder_path(known_folders::KnownFolder::SavedGames)
});
#[cfg(not(windows))]
static SAVED_GAMES: Option<PathBuf> = None;
match self {
Self::Config => CONFIG.clone(),
Self::Data => DATA.clone(),
Self::DataLocal => DATA_LOCAL.clone(),
Self::DataLocalLow => DATA_LOCAL_LOW.clone(),
Self::Document => DOCUMENT.clone(),
Self::Home => HOME.clone(),
Self::Public => PUBLIC.clone(),
Self::SavedGames => SAVED_GAMES.clone(),
}
}
}

View File

@ -1,51 +0,0 @@
use std::sync::LazyLock;
pub const ALL: &[&str] = &[
ROOT,
GAME,
BASE,
HOME,
STORE_USER_ID,
OS_USER_NAME,
WIN_APP_DATA,
WIN_LOCAL_APP_DATA,
WIN_DOCUMENTS,
WIN_PUBLIC,
WIN_PROGRAM_DATA,
WIN_DIR,
XDG_DATA,
XDG_CONFIG,
];
/// These are paths where `<placeholder>/*/` is suspicious.
pub const AVOID_WILDCARDS: &[&str] = &[
ROOT,
HOME,
WIN_APP_DATA,
WIN_LOCAL_APP_DATA,
WIN_DOCUMENTS,
WIN_PUBLIC,
WIN_PROGRAM_DATA,
WIN_DIR,
XDG_DATA,
XDG_CONFIG,
];
pub const ROOT: &str = "<root>"; // a directory where games are installed (configured in backup tool)
pub const GAME: &str = "<game>"; // an installDir (if defined) or the game's canonical name in the manifest
pub const BASE: &str = "<base>"; // shorthand for <root>/<game> (unless overridden by store-specific rules)
pub const HOME: &str = "<home>"; // current user's home directory in the OS (~)
pub const STORE_USER_ID: &str = "<storeUserId>"; // a store-specific id from the manifest, corresponding to the root's store type
pub const OS_USER_NAME: &str = "<osUserName>"; // current user's ID in the game store
pub const WIN_APP_DATA: &str = "<winAppData>"; // current user's name in the OS
pub const WIN_LOCAL_APP_DATA: &str = "<winLocalAppData>"; // %APPDATA% on Windows
pub const WIN_LOCAL_APP_DATA_LOW: &str = "<winLocalAppDataLow>"; // %LOCALAPPDATA% on Windows
pub const WIN_DOCUMENTS: &str = "<winDocuments>"; // <home>/AppData/LocalLow on Windows
pub const WIN_PUBLIC: &str = "<winPublic>"; // <home>/Documents (f.k.a. <home>/My Documents) or a localized equivalent on Windows
pub const WIN_PROGRAM_DATA: &str = "<winProgramData>"; // %PUBLIC% on Windows
pub const WIN_DIR: &str = "<winDir>"; // %PROGRAMDATA% on Windows
pub const XDG_DATA: &str = "<xdgData>"; // %WINDIR% on Windows
pub const XDG_CONFIG: &str = "<xdgConfig>"; // $XDG_DATA_HOME on Linux
pub const SKIP: &str = "<skip>"; // $XDG_CONFIG_HOME on Linux
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(|| whoami::username());

View File

@ -1,255 +0,0 @@
use std::{
fs::{self, create_dir_all, File},
io::{self, ErrorKind, Read, Write},
path::{Path, PathBuf}, thread::sleep, time::Duration,
};
use super::{
backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*,
};
use log::{debug, warn};
use rustix::path::Arg;
use tempfile::tempfile;
use crate::{
database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform,
};
use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
let f = File::create_new("save").unwrap();
let compressor = zstd::Encoder::new(f, 22).unwrap();
let mut tarball = tar::Builder::new(compressor);
let manager = BackupManager::new();
for file in meta.files.iter_mut() {
let id = uuid::Uuid::new_v4().to_string();
let os = match file
.conditions
.iter()
.find_map(|p| match p {
super::conditions::Condition::Os(os) => Some(os),
_ => None,
})
.cloned()
{
Some(os) => os,
None => {
warn!(
"File {:?} could not be backed up because it did not provide an OS",
&file
);
continue;
}
};
let handler = match manager.sources.get(&(manager.current_platform, os)) {
Some(h) => *h,
None => continue,
};
let t_path = PathBuf::from(normalize(&file.path, os));
println!("{:?}", &t_path);
let path = parse_path(t_path, handler, &meta.game_version).unwrap();
let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here
if f.is_dir() {
tarball.append_dir_all(&id, path).unwrap();
} else if f.is_file() {
tarball
.append_file(&id, &mut File::open(path).unwrap())
.unwrap();
}
file.id = Some(id);
}
let binding = serde_json::to_string(meta).unwrap();
println!("Binding: {}", &binding);
let serialized = binding.as_bytes();
let mut file = tempfile().unwrap();
file.write(serialized).unwrap();
tarball.append_file("metadata", &mut file).unwrap();
tarball.into_inner().unwrap().finish().unwrap()
}
pub fn extract(file: PathBuf) -> Result<(), BackupError> {
let tmpdir = tempfile::tempdir().unwrap();
// Reopen the file for reading
let file = File::open(file).unwrap();
let decompressor = zstd::Decoder::new(file).unwrap();
let mut f = tar::Archive::new(decompressor);
f.unpack(tmpdir.path()).unwrap();
let path = tmpdir.path();
let mut manifest = File::open(path.join("metadata")).unwrap();
let mut manifest_slice = Vec::new();
manifest.read_to_end(&mut manifest_slice).unwrap();
let manifest: CloudSaveMetadata = serde_json::from_slice(&manifest_slice).unwrap();
for file in manifest.files {
let current_path = path.join(file.id.as_ref().unwrap());
let manager = BackupManager::new();
let os = match file
.conditions
.iter()
.find_map(|p| match p {
super::conditions::Condition::Os(os) => Some(os),
_ => None,
})
.cloned()
{
Some(os) => os,
None => {
warn!(
"File {:?} could not be replaced up because it did not provide an OS",
&file
);
continue;
}
};
let handler = match manager.sources.get(&(manager.current_platform, os)) {
Some(h) => *h,
None => continue,
};
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
create_dir_all(&new_path.parent().unwrap()).unwrap();
println!("Current path {:?} copying to {:?}", &current_path, &new_path);
copy_item(current_path, new_path).unwrap();
}
Ok(())
}
pub fn copy_item<P: AsRef<Path>>(src: P, dest: P) -> io::Result<()> {
let src_path = src.as_ref();
let dest_path = dest.as_ref();
let metadata = fs::metadata(&src_path)?;
if metadata.is_file() {
// Ensure the parent directory of the destination exists for a file copy
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src_path, &dest_path)?;
} else if metadata.is_dir() {
// For directories, we call the recursive helper function.
// The destination for the recursive copy is the `dest_path` itself.
copy_dir_recursive(&src_path, &dest_path)?;
} else {
// Handle other file types like symlinks if necessary,
// for now, return an error or skip.
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Source {:?} is neither a file nor a directory", src_path),
));
}
Ok(())
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
fs::create_dir_all(&dest)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let entry_path = entry.path();
let entry_file_name = match entry_path.file_name() {
Some(name) => name,
None => continue, // Skip if somehow there's no file name
};
let dest_entry_path = dest.join(entry_file_name);
let metadata = entry.metadata()?;
if metadata.is_file() {
debug!("Writing file {} to {}", entry_path.display(), dest_entry_path.display());
fs::copy(&entry_path, &dest_entry_path)?;
} else if metadata.is_dir() {
copy_dir_recursive(&entry_path, &dest_entry_path)?;
}
// Ignore other types like symlinks for this basic implementation
}
Ok(())
}
pub fn parse_path(
path: PathBuf,
backup_handler: &dyn BackupHandler,
game: &GameVersion,
) -> Result<PathBuf, BackupError> {
println!("Parsing: {:?}", &path);
let mut s = PathBuf::new();
for component in path.components() {
match component.as_str().unwrap() {
ROOT => s.push(backup_handler.root_translate(&path, game)?),
GAME => s.push(backup_handler.game_translate(&path, game)?),
BASE => s.push(backup_handler.base_translate(&path, game)?),
HOME => s.push(backup_handler.home_translate(&path, game)?),
STORE_USER_ID => s.push(backup_handler.store_user_id_translate(&path, game)?),
OS_USER_NAME => s.push(backup_handler.os_user_name_translate(&path, game)?),
WIN_APP_DATA => s.push(backup_handler.win_app_data_translate(&path, game)?),
WIN_LOCAL_APP_DATA => s.push(backup_handler.win_local_app_data_translate(&path, game)?),
WIN_LOCAL_APP_DATA_LOW => {
s.push(backup_handler.win_local_app_data_low_translate(&path, game)?)
}
WIN_DOCUMENTS => s.push(backup_handler.win_documents_translate(&path, game)?),
WIN_PUBLIC => s.push(backup_handler.win_public_translate(&path, game)?),
WIN_PROGRAM_DATA => s.push(backup_handler.win_program_data_translate(&path, game)?),
WIN_DIR => s.push(backup_handler.win_dir_translate(&path, game)?),
XDG_DATA => s.push(backup_handler.xdg_data_translate(&path, game)?),
XDG_CONFIG => s.push(backup_handler.xdg_config_translate(&path, game)?),
SKIP => s.push(backup_handler.skip_translate(&path, game)?),
_ => s.push(PathBuf::from(component.as_os_str())),
}
}
println!("Final line: {:?}", &s);
Ok(s)
}
pub fn test() {
let mut meta = CloudSaveMetadata {
files: vec![
GameFile {
path: String::from("<home>/favicon.png"),
id: None,
data_type: super::metadata::DataType::File,
tags: Vec::new(),
conditions: vec![Condition::Os(Platform::Linux)],
},
GameFile {
path: String::from("<home>/Documents/Pixel Art"),
id: None,
data_type: super::metadata::DataType::File,
tags: Vec::new(),
conditions: vec![Condition::Os(Platform::Linux)],
},
],
game_version: GameVersion {
game_id: String::new(),
version_name: String::new(),
platform: Platform::Linux,
launch_command: String::new(),
launch_args: Vec::new(),
launch_command_template: String::new(),
setup_command: String::new(),
setup_args: Vec::new(),
setup_command_template: String::new(),
only_setup: true,
version_index: 0,
delta: false,
umu_id_override: None,
},
save_id: String::from("aaaaaaa"),
};
//resolve(&mut meta);
extract("save".into()).unwrap();
}

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

@ -7,6 +7,7 @@ use std::{
}; };
use chrono::Utc; use chrono::Utc;
use directories::BaseDirs;
use log::{debug, error, info}; use log::{debug, error, info};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
@ -27,7 +28,6 @@ pub struct DatabaseAuth {
pub private: String, pub private: String,
pub cert: String, pub cert: String,
pub client_id: String, pub client_id: String,
pub web_token: Option<String>,
} }
// Strings are version names for a particular game // Strings are version names for a particular game
@ -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 {},
@ -54,11 +54,7 @@ pub enum ApplicationTransientStatus {
Running {}, Running {},
} }
fn default_template() -> String { #[derive(Serialize, Deserialize, Clone, Debug)]
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GameVersion { pub struct GameVersion {
pub game_id: String, pub game_id: String,
@ -68,13 +64,9 @@ pub struct GameVersion {
pub launch_command: String, pub launch_command: String,
pub launch_args: Vec<String>, pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String, pub setup_command: String,
pub setup_args: Vec<String>, pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool, pub only_setup: bool,
@ -106,14 +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>>( fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
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()],
@ -125,14 +112,15 @@ 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,
},
} }
} }
} }
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop"))); LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
// Custom JSON serializer to support everything we need // Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -162,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()
@ -199,12 +185,25 @@ impl DatabaseImpls for DatabaseInterface {
} }
} }
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
app_handle: &AppHandle,
meta: DownloadableMetadata,
setter: F,
) {
let mut db_handle = borrow_db_mut_checked();
setter(&mut db_handle, &meta);
drop(db_handle);
save_db();
let status = GameStatusManager::fetch_state(&meta.id);
push_game_update(app_handle, &meta.id, status);
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error // TODO: Make the error relelvant rather than just assume that it's a Deserialize error
fn handle_invalid_database( 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();
@ -221,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

@ -1,21 +0,0 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(Debug, SerializeDisplay, Clone, Copy)]
pub enum BackupError {
InvalidSystem,
NotFound,
ParseError
}
impl Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
BackupError::NotFound => "Could not generate or find path",
BackupError::ParseError => "Failed to parse path",
};
write!(f, "{}", s)
}
}

View File

@ -4,4 +4,3 @@ pub mod library_error;
pub mod process_error; pub mod process_error;
pub mod remote_access_error; pub mod remote_access_error;
pub mod setup_error; pub mod setup_error;
pub mod backup_error;

View File

@ -11,7 +11,6 @@ pub enum ProcessError {
InvalidID, InvalidID,
InvalidVersion, InvalidVersion,
IOError(Error), IOError(Error),
FormatError(String), // String errors supremacy
InvalidPlatform, InvalidPlatform,
} }
@ -26,7 +25,6 @@ impl Display for ProcessError {
ProcessError::InvalidVersion => "Invalid Game version", ProcessError::InvalidVersion => "Invalid Game version",
ProcessError::IOError(error) => &error.to_string(), ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform", ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {}", e),
}; };
write!(f, "{}", s) write!(f, "{}", s)
} }

View File

@ -1,5 +1,4 @@
use std::{ use std::{
any::{Any, TypeId},
error::Error, error::Error,
fmt::{Display, Formatter}, fmt::{Display, Formatter},
sync::Arc, sync::Arc,
@ -11,30 +10,24 @@ 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),
InvalidEndpoint, InvalidEndpoint,
HandshakeFailed(String), HandshakeFailed(String),
GameNotFound(String), GameNotFound,
InvalidResponse(DropServerError), InvalidResponse(DropServerError),
InvalidRedirect, InvalidRedirect,
ManifestDownloadFailed(StatusCode, String), ManifestDownloadFailed(StatusCode, String),
OutOfSync, OutOfSync,
Cache(cacache::Error),
Generic(String), Generic(String),
} }
impl Display for RemoteAccessError { impl Display for RemoteAccessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
RemoteAccessError::FetchError(error) => { RemoteAccessError::FetchError(error) => write!(
if error.is_connect() {
return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again.");
}
write!(
f, f,
"{}: {}", "{}: {}",
error, error,
@ -43,14 +36,13 @@ impl Display for RemoteAccessError {
.map(|e| e.to_string()) .map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string())) .or_else(|| Some("Unknown error".to_string()))
.unwrap() .unwrap()
) ),
},
RemoteAccessError::ParsingError(parse_error) => { RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error) write!(f, "{}", parse_error)
} }
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"), RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message), RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {}", id), RemoteAccessError::GameNotFound => write!(f, "could not find game on server"),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message), RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"), RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!( RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
@ -60,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,24 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::games::library::Game;
pub type Collections = Vec<Collection>;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct Collection {
id: String,
name: String,
is_default: bool,
user_id: String,
entries: Vec<CollectionObject>
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct CollectionObject {
collection_id: String,
game_id: String,
game: Game,
}

View File

@ -1,85 +0,0 @@
use reqwest::blocking::Client;
use serde_json::json;
use url::Url;
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, DB};
use super::collection::{Collection, Collections};
#[tauri::command]
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
let client = Client::new();
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send()?;
Ok(response.json()?)
}
#[tauri::command]
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
let client = Client::new();
let response = make_request(&client, &["/api/v1/client/collection/", &collection_id], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send()?;
Ok(response.json()?)
}
#[tauri::command]
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
let client = Client::new();
let base_url = DB.fetch_base_url();
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
let response = client
.post(base_url)
.header("Authorization", generate_authorization_header())
.json(&json!({"name": name}))
.send()?;
Ok(response.json()?)
}
#[tauri::command]
pub fn add_game_to_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> {
let client = Client::new();
let url = Url::parse(&format!("{}api/v1/client/collection/{}/entry/", DB.fetch_base_url(), collection_id))?;
client
.post(url)
.header("Authorization", generate_authorization_header())
.json(&json!({"id": game_id}))
.send()?;
Ok(())
}
#[tauri::command]
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
let client = Client::new();
let base_url = Url::parse(&format!("{}api/v1/client/collection/{}", DB.fetch_base_url(), collection_id))?;
let response = client
.delete(base_url)
.header("Authorization", generate_authorization_header())
.send()?;
Ok(response.json()?)
}
#[tauri::command]
pub fn delete_game_in_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> {
let client = Client::new();
let base_url = Url::parse(&format!("{}api/v1/client/collection/{}/entry", DB.fetch_base_url(), collection_id))?;
client
.delete(base_url)
.header("Authorization", generate_authorization_header())
.json(&json!({"id": game_id}))
.send()?;
Ok(())
}

View File

@ -1,2 +0,0 @@
pub mod commands;
pub mod collection;

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

@ -1,6 +1,6 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::database::db::{ use crate::database::db::{
borrow_db_checked, ApplicationTransientStatus, DatabaseImpls, borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
GameDownloadStatus, GameDownloadStatus,
}; };
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
@ -99,7 +99,6 @@ impl GameDownloadAgent {
push_game_update( push_game_update(
app_handle, app_handle,
&self.metadata().id, &self.metadata().id,
None,
( (
None, None,
Some(ApplicationTransientStatus::Downloading { Some(ApplicationTransientStatus::Downloading {
@ -136,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()))?;
@ -251,7 +250,6 @@ impl GameDownloadAgent {
let completed_indexes_loop_arc = completed_indexes.clone(); let completed_indexes_loop_arc = completed_indexes.clone();
let contexts = self.contexts.lock().unwrap(); let contexts = self.contexts.lock().unwrap();
debug!("{:#?}", contexts);
pool.scope(|scope| { pool.scope(|scope| {
let client = &reqwest::blocking::Client::new(); let client = &reqwest::blocking::Client::new();
for (index, context) in contexts.iter().enumerate() { for (index, context) in contexts.iter().enumerate() {
@ -282,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 |_| {
@ -368,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())
@ -376,8 +370,9 @@ impl Downloadable for GameDownloadAgent {
error!("error while managing download: {}", error); error!("error while managing download: {}", error);
let mut handle = DB.borrow_data_mut().unwrap(); set_game_status(app_handle, self.metadata(), |db_handle, meta| {
handle.applications.transient_statuses.remove(&self.metadata()); db_handle.applications.transient_statuses.remove(meta);
});
} }
fn on_complete(&self, app_handle: &tauri::AppHandle) { fn on_complete(&self, app_handle: &tauri::AppHandle) {
@ -399,7 +394,6 @@ impl Downloadable for GameDownloadAgent {
GameUpdateEvent { GameUpdateEvent {
game_id: meta.id.clone(), game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None), status: (Some(GameDownloadStatus::Remote {}), None),
version: None,
}, },
) )
.unwrap(); .unwrap();

View File

@ -2,7 +2,7 @@ use std::fs::remove_dir_all;
use std::sync::Mutex; use std::sync::Mutex;
use std::thread::spawn; use std::thread::spawn;
use log::{debug, error, info, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::Emitter; use tauri::Emitter;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
@ -11,22 +11,19 @@ use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, Gam
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata; use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::library_error::LibraryError;
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, get_cached_object_db};
use crate::remote::requests::make_request; use crate::remote::requests::make_request;
use crate::{AppState, DB}; use crate::AppState;
#[derive(Serialize, Deserialize)] #[derive(serde::Serialize)]
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
version: Option<GameVersion>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Game { pub struct Game {
id: String, id: String,
@ -39,7 +36,6 @@ pub struct Game {
m_banner_id: String, m_banner_id: String,
m_cover_id: String, m_cover_id: String,
m_image_library: Vec<String>, m_image_library: Vec<String>,
m_image_carousel: Vec<String>,
} }
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent { pub struct GameUpdateEvent {
@ -48,7 +44,6 @@ pub struct GameUpdateEvent {
Option<GameDownloadStatus>, Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>, Option<ApplicationTransientStatus>,
), ),
pub version: Option<GameVersion>,
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@ -71,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();
@ -88,8 +81,9 @@ pub fn fetch_library_logic(
return Err(RemoteAccessError::InvalidResponse(err)); return Err(RemoteAccessError::InvalidResponse(err));
} }
let mut 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();
@ -104,63 +98,18 @@ pub fn fetch_library_logic(
} }
} }
// Add games that are installed but no longer in library
for (_, meta) in &db_handle.applications.installed_game_version {
if games.iter().find(|e| e.id == meta.id).is_some() {
continue;
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = get_cached_object_db::<String, Game>(meta.id.clone(), &db_handle)?;
games.push(game);
}
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 handle = DB.borrow_data().unwrap();
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => Some(
handle
.applications
.game_versions
.get(&metadata.id)
.unwrap()
.get(metadata.version.as_ref().unwrap())
.unwrap()
.clone(),
),
};
drop(handle);
let game = state_handle.games.get(&id); let game = state_handle.games.get(&id);
if let Some(game) = game { if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id); let status = GameStatusManager::fetch_state(&id);
@ -168,21 +117,18 @@ pub fn fetch_game_logic(
let data = FetchGameStruct { let data = FetchGameStruct {
game: game.clone(), game: game.clone(),
status, status,
version,
}; };
cache_object(id, game)?;
return Ok(data); return Ok(data);
} }
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| { let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header()) r.header("Authorization", generate_authorization_header())
})? })?
.send()?; .send()?;
if response.status() == 404 { if response.status() == 404 {
return Err(RemoteAccessError::GameNotFound(id)); return Err(RemoteAccessError::GameNotFound);
} }
if response.status() != 200 { if response.status() != 200 {
let err = response.json().unwrap(); let err = response.json().unwrap();
@ -207,45 +153,11 @@ pub fn fetch_game_logic(
let data = FetchGameStruct { let data = FetchGameStruct {
game: game.clone(), game: game.clone(),
status, status,
version,
}; };
cache_object(id, &game)?;
Ok(data) Ok(data)
} }
pub fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = DB.borrow_data().unwrap();
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => Some(
handle
.applications
.game_versions
.get(&metadata.id)
.unwrap()
.get(metadata.version.as_ref().unwrap())
.unwrap()
.clone(),
),
};
drop(handle);
let status = GameStatusManager::fetch_state(&id);
let game = get_cached_object::<String, Game>(id)?;
Ok(FetchGameStruct {
game,
status,
version,
})
}
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>>,
@ -281,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
@ -292,7 +204,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
push_game_update( push_game_update(
app_handle, app_handle,
&meta.id, &meta.id,
None,
(None, Some(ApplicationTransientStatus::Uninstalling {})), (None, Some(ApplicationTransientStatus::Uninstalling {})),
); );
@ -302,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,
@ -319,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();
@ -330,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
@ -343,12 +248,10 @@ 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,
&meta.id, &meta.id,
None,
(Some(GameDownloadStatus::Remote {}), None), (Some(GameDownloadStatus::Remote {}), None),
); );
} }
@ -371,7 +274,7 @@ pub fn on_game_complete(
) -> Result<(), RemoteAccessError> { ) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote // Fetch game version information from remote
if meta.version.is_none() { if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone())); return Err(RemoteAccessError::GameNotFound);
} }
let header = generate_authorization_header(); let header = generate_authorization_header();
@ -379,7 +282,7 @@ pub fn on_game_complete(
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = make_request( let response = make_request(
&client, &client,
&["/api/v1/client/game/version"], &["/api/v1/client/metadata/version"],
&[ &[
("id", &meta.id), ("id", &meta.id),
("version", meta.version.as_ref().unwrap()), ("version", meta.version.as_ref().unwrap()),
@ -388,7 +291,7 @@ pub fn on_game_complete(
)? )?
.send()?; .send()?;
let game_version: GameVersion = response.json()?; let data: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
handle handle
@ -396,7 +299,7 @@ pub fn on_game_complete(
.game_versions .game_versions
.entry(meta.id.clone()) .entry(meta.id.clone())
.or_default() .or_default()
.insert(meta.version.clone().unwrap(), game_version.clone()); .insert(meta.version.clone().unwrap(), data.clone());
handle handle
.applications .applications
.installed_game_version .installed_game_version
@ -405,7 +308,7 @@ pub fn on_game_complete(
drop(handle); drop(handle);
save_db(); save_db();
let status = if game_version.setup_command.is_empty() { let status = if data.setup_command.is_empty() {
GameDownloadStatus::Installed { GameDownloadStatus::Installed {
version_name: meta.version.clone().unwrap(), version_name: meta.version.clone().unwrap(),
install_dir, install_dir,
@ -430,7 +333,6 @@ pub fn on_game_complete(
GameUpdateEvent { GameUpdateEvent {
game_id: meta.id.clone(), game_id: meta.id.clone(),
status: (Some(status), None), status: (Some(status), None),
version: Some(game_version),
}, },
) )
.unwrap(); .unwrap();
@ -438,63 +340,14 @@ pub fn on_game_complete(
Ok(()) Ok(())
} }
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, version: Option<GameVersion>, status: GameStatusWithTransient) { pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", game_id), &format!("update_game/{}", game_id),
GameUpdateEvent { GameUpdateEvent {
game_id: game_id.clone(), game_id: game_id.clone(),
status, status,
version,
}, },
) )
.unwrap(); .unwrap();
} }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendGameOptions {
launch_string: String,
}
#[tauri::command]
pub fn update_game_configuration(
game_id: String,
options: FrontendGameOptions,
) -> Result<(), LibraryError> {
let mut handle = DB.borrow_data_mut().unwrap();
let installed_version = handle
.applications
.installed_game_version
.get(&game_id)
.ok_or(LibraryError::MetaNotFound(game_id))?;
let id = installed_version.id.clone();
let version = installed_version.version.clone().unwrap();
let mut existing_configuration = handle
.applications
.game_versions
.get(&id)
.unwrap()
.get(&version)
.unwrap()
.clone();
// Add more options in here
existing_configuration.launch_command_template = options.launch_string;
// Add no more options past here
handle
.applications
.game_versions
.get_mut(&id)
.unwrap()
.insert(version.to_string(), existing_configuration);
drop(handle);
save_db();
Ok(())
}

View File

@ -2,4 +2,3 @@ pub mod commands;
pub mod downloads; pub mod downloads;
pub mod library; pub mod library;
pub mod state; pub mod state;
pub mod collections;

View File

@ -3,7 +3,6 @@ mod games;
mod autostart; mod autostart;
mod cleanup; mod cleanup;
mod cloud_saves;
mod commands; mod commands;
mod download_manager; mod download_manager;
mod error; mod error;
@ -13,7 +12,6 @@ mod remote;
use crate::database::db::DatabaseImpls; use crate::database::db::DatabaseImpls;
use autostart::{get_autostart_enabled, toggle_autostart}; use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit}; use cleanup::{cleanup_and_exit, quit};
use cloud_saves::resolver::test;
use commands::fetch_state; use commands::fetch_state;
use database::commands::{ use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings, add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
@ -27,15 +25,11 @@ use download_manager::commands::{
}; };
use download_manager::download_manager::DownloadManager; use download_manager::download_manager::DownloadManager;
use download_manager::download_manager_builder::DownloadManagerBuilder; use download_manager::download_manager_builder::DownloadManagerBuilder;
use games::collections::commands::{
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
fetch_collection, fetch_collections,
};
use games::commands::{ use games::commands::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
}; };
use games::downloads::commands::download_game; use games::downloads::commands::download_game;
use games::library::{update_game_configuration, Game}; use games::library::Game;
use http::Response; use http::Response;
use http::{header::*, response::Builder as ResponseBuilder}; use http::{header::*, response::Builder as ResponseBuilder};
use log::{debug, info, warn, LevelFilter}; use log::{debug, info, warn, LevelFilter};
@ -46,15 +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, auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
sign_out, use_remote,
}; };
use remote::fetch_object::{fetch_object, fetch_object_offline};
use remote::requests::make_request; use remote::requests::make_request;
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
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;
@ -70,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,
@ -91,7 +80,6 @@ pub struct User {
profile_picture: String, profile_picture: String,
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AppState<'a> { pub struct AppState<'a> {
@ -106,7 +94,6 @@ pub struct AppState<'a> {
} }
fn setup(handle: AppHandle) -> AppState<'static> { fn setup(handle: AppHandle) -> AppState<'static> {
test();
let logfile = FileAppender::builder() let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new( .encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}", "{d} | {l} | {f}:{L} - {m}{n}",
@ -249,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,
@ -258,14 +244,6 @@ pub fn run() {
fetch_download_dir_stats, fetch_download_dir_stats,
fetch_game_status, fetch_game_status,
fetch_game_verion_options, fetch_game_verion_options,
update_game_configuration,
// Collections
fetch_collections,
fetch_collection,
create_collection,
add_game_to_collection,
delete_collection,
delete_game_in_collection,
// Downloads // Downloads
download_game, download_game,
move_download_in_queue, move_download_in_queue,
@ -349,7 +327,7 @@ pub fn run() {
} }
_ => { _ => {
warn!("menu event not handled: {:?}", event.id); println!("menu event not handled: {:?}", event.id);
} }
}) })
.build(app) .build(app)
@ -378,26 +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!( let object_id = &request.uri().path()[1..];
state,
fetch_object,
fetch_object_offline,
request,
responder
);
})
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
offline!(
state,
handle_server_proto,
handle_server_proto_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 {

View File

@ -4,13 +4,10 @@ use std::{
io::{self, Error}, io::{self, Error},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Child, Command, ExitStatus}, process::{Child, Command, ExitStatus},
str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::spawn, thread::spawn,
}; };
use dynfmt::Format;
use dynfmt::SimpleCurlyFormat;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared_child::SharedChild; use shared_child::SharedChild;
@ -19,8 +16,7 @@ use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{ use crate::{
database::db::{ database::db::{
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR
DATA_ROOT_DIR,
}, },
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError, error::process_error::ProcessError,
@ -43,14 +39,11 @@ impl ProcessManager<'_> {
drop(root_dir_lock); drop(root_dir_lock);
ProcessManager { ProcessManager {
#[cfg(target_os = "windows")] current_platform: if cfg!(windows) {
current_platform: Platform::Windows, Platform::Windows
} else {
#[cfg(target_os = "macos")] Platform::Linux
current_platform: Platform::MacOs, },
#[cfg(target_os = "linux")]
current_platform: Platform::Linux,
app_handle, app_handle,
processes: HashMap::new(), processes: HashMap::new(),
@ -65,10 +58,6 @@ impl ProcessManager<'_> {
(Platform::Linux, Platform::Linux), (Platform::Linux, Platform::Linux),
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
), ),
(
(Platform::MacOs, Platform::MacOs),
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
( (
(Platform::Linux, Platform::Windows), (Platform::Linux, Platform::Windows),
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), &UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
@ -77,6 +66,20 @@ impl ProcessManager<'_> {
} }
} }
fn process_command(&self, install_dir: &String, command: Vec<String>) -> (PathBuf, Vec<String>) {
let root = &command[0];
let install_dir = Path::new(install_dir);
let absolute_exe = install_dir.join(root);
/*
let args = command_components[1..]
.iter()
.map(|v| v.to_string())
.collect();
*/
(absolute_exe, Vec::new())
}
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> { pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
match self.processes.get(&game_id) { match self.processes.get(&game_id) {
Some(child) => { Some(child) => {
@ -134,7 +137,7 @@ impl ProcessManager<'_> {
let status = GameStatusManager::fetch_state(&game_id); let status = GameStatusManager::fetch_state(&game_id);
push_game_update(&self.app_handle, &game_id, None, status); push_game_update(&self.app_handle, &game_id, status);
// TODO better management // TODO better management
} }
@ -195,6 +198,7 @@ impl ProcessManager<'_> {
_ => return Err(ProcessError::NotDownloaded), _ => return Err(ProcessError::NotDownloaded),
}; };
let game_version = db_lock let game_version = db_lock
.applications .applications
.game_versions .game_versions
@ -203,6 +207,37 @@ impl ProcessManager<'_> {
.get(version_name) .get(version_name)
.ok_or(ProcessError::InvalidVersion)?; .ok_or(ProcessError::InvalidVersion)?;
let mut command: Vec<String> = Vec::new();
match game_status {
GameDownloadStatus::Installed {
version_name: _,
install_dir: _,
} => {
command.extend([game_version.launch_command.clone()]);
command.extend(game_version.launch_args.clone());
},
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
} => {
command.extend([game_version.setup_command.clone()]);
command.extend(game_version.setup_args.clone());
},
_ => panic!("unreachable code"),
};
info!("Command: {:?}", &command);
let (command, args) = self.process_command(install_dir, command);
let target_current_dir = command.parent().unwrap().to_str().unwrap();
info!(
"launching process {} in {}",
command.to_str().unwrap(),
target_current_dir
);
let current_time = chrono::offset::Local::now(); let current_time = chrono::offset::Local::now();
let log_file = OpenOptions::new() let log_file = OpenOptions::new()
.write(true) .write(true)
@ -238,55 +273,19 @@ impl ProcessManager<'_> {
.get(&(current_platform, target_platform)) .get(&(current_platform, target_platform))
.ok_or(ProcessError::InvalidPlatform)?; .ok_or(ProcessError::InvalidPlatform)?;
let (launch, args) = match game_status { let launch_process = game_launcher
GameDownloadStatus::Installed { .launch_process(
version_name: _,
install_dir: _,
} => (&game_version.launch_command, &game_version.launch_args),
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
} => (&game_version.setup_command, &game_version.setup_args),
GameDownloadStatus::Remote {} => unreachable!("nuh uh"),
};
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
let launch = launch.to_str().unwrap();
let launch_string = game_launcher.create_launch_process(
&meta, &meta,
launch.to_string(), command.to_string_lossy().to_string(),
args.to_vec(),
game_version, game_version,
install_dir, target_current_dir,
); log_file,
error_file,
let launch_string = SimpleCurlyFormat )
.format(&game_version.launch_command_template, &[launch_string]) .map_err(ProcessError::IOError)?;
.map_err(|e| ProcessError::FormatError(e.to_string()))?
.to_string();
#[cfg(target_os = "windows")]
let mut command = Command::new("cmd");
#[cfg(target_os = "windows")]
command.args(["/C", &launch_string]);
info!("launching (in {}): {}", install_dir, launch_string,);
#[cfg(unix)]
let mut command: Command = Command::new("sh");
#[cfg(unix)]
command.arg("-c").arg(launch_string);
command
.stderr(error_file)
.stdout(log_file)
.current_dir(install_dir);
let child = command.spawn().map_err(ProcessError::IOError)?;
let launch_process_handle = let launch_process_handle =
Arc::new(SharedChild::new(child).map_err(ProcessError::IOError)?); Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?);
db_lock db_lock
.applications .applications
@ -296,7 +295,6 @@ impl ProcessManager<'_> {
push_game_update( push_game_update(
&self.app_handle, &self.app_handle,
&meta.id, &meta.id,
None,
(None, Some(ApplicationTransientStatus::Running {})), (None, Some(ApplicationTransientStatus::Running {})),
); );
@ -324,105 +322,66 @@ impl ProcessManager<'_> {
} }
} }
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)] #[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
pub enum Platform { pub enum Platform {
Windows, Windows,
Linux, Linux,
MacOs,
}
impl Platform {
const WINDOWS: bool = cfg!(target_os = "windows");
const MAC: bool = cfg!(target_os = "macos");
const LINUX: bool = cfg!(target_os = "linux");
#[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")]
pub const HOST: Platform = Self::MacOs;
#[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool {
match self {
Self::Windows | Self::MacOs => false,
Self::Linux => true,
}
}
}
impl From<&str> for Platform {
fn from(value: &str) -> Self {
match value.to_lowercase().trim() {
"windows" => Self::Windows,
"linux" => Self::Linux,
"mac" | "macos" => Self::MacOs,
_ => unimplemented!(),
}
}
}
impl From<whoami::Platform> for Platform {
fn from(value: whoami::Platform) -> Self {
match value {
whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::MacOs,
_ => unimplemented!()
}
}
} }
pub trait ProcessHandler: Send + 'static { pub trait ProcessHandler: Send + 'static {
fn create_launch_process( fn launch_process(
&self, &self,
meta: &DownloadableMetadata, meta: &DownloadableMetadata,
launch_command: String, launch_command: String,
args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
current_dir: &str, current_dir: &str,
) -> String; log_file: File,
error_file: File,
) -> Result<Child, Error>;
} }
struct NativeGameLauncher; struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher { impl ProcessHandler for NativeGameLauncher {
fn create_launch_process( fn launch_process(
&self, &self,
_meta: &DownloadableMetadata, _meta: &DownloadableMetadata,
launch_command: String, launch_command: String,
args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
current_dir: &str, current_dir: &str,
) -> String { log_file: File,
format!("\"{}\" {}", launch_command, args.join(" ")) error_file: File,
) -> Result<Child, Error> {
Command::new(PathBuf::from(launch_command))
.current_dir(current_dir)
.stdout(log_file)
.stderr(error_file)
.args(game_version.launch_args.clone())
.spawn()
} }
} }
const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run"; const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
struct UMULauncher; struct UMULauncher;
impl ProcessHandler for UMULauncher { impl ProcessHandler for UMULauncher {
fn create_launch_process( fn launch_process(
&self, &self,
_meta: &DownloadableMetadata, _meta: &DownloadableMetadata,
launch_command: String, launch_command: String,
args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
_current_dir: &str, _current_dir: &str,
) -> String { _log_file: File,
debug!("Game override: \"{:?}\"", &game_version.umu_id_override); _error_file: File,
) -> Result<Child, Error> {
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 Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
.is_empty() None => game_version.game_id.clone()
.then_some(game_version.game_id.clone())
.unwrap_or(game_override.clone()),
None => game_version.game_id.clone(),
}; };
format!( info!("Game ID: {}", game_id);
"GAMEID={game_id} {umu} \"{launch}\" {args}", UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command)
umu = UMU_LAUNCHER_EXECUTABLE, .game_id(game_id)
launch = launch_command, .launch_args(game_version.launch_args.clone())
args = args.join(" ") .build()
) .spawn()
} }
} }

View File

@ -1,11 +1,9 @@
use std::{env, sync::Mutex}; use std::{env, sync::Mutex};
use chrono::Utc; use chrono::Utc;
use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname;
use log::{debug, error, warn}; use log::{debug, error, warn};
use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
@ -17,10 +15,7 @@ use crate::{
AppState, AppStatus, User, DB, AppState, AppStatus, User, DB,
}; };
use super::{ use super::requests::make_request;
cache::{cache_object, get_cached_object},
requests::make_request,
};
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -44,6 +39,20 @@ struct HandshakeResponse {
id: String, id: String,
} }
// TODO: Change return value on Err
pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
let mut signer = Signer::new(MessageDigest::sha256(), &pkey_private_key).unwrap();
signer.update(nonce.as_bytes()).unwrap();
let signature = signer.sign_to_vec().unwrap();
let hex_signature = hex::encode(signature);
Ok(hex_signature)
}
pub fn generate_authorization_header() -> String { pub fn generate_authorization_header() -> String {
let certs = { let certs = {
let db = borrow_db_checked(); let db = borrow_db_checked();
@ -114,51 +123,16 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
private: response_struct.private, private: response_struct.private,
cert: response_struct.certificate, cert: response_struct.certificate,
client_id: response_struct.id, client_id: response_struct.id,
web_token: None, // gets created later
}); });
drop(handle); drop(handle);
save_db(); save_db();
} }
let web_token = {
let header = generate_authorization_header();
let token = client
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
.header("Authorization", header)
.send()
.unwrap();
token.text().unwrap()
};
let mut handle = borrow_db_mut_checked();
let mut_auth = handle.auth.as_mut().unwrap();
mut_auth.web_token = Some(web_token);
drop(handle);
save_db();
{ {
let app_state = app.state::<Mutex<AppState>>(); let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap(); let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedIn; app_state_handle.status = AppStatus::SignedIn;
app_state_handle.user = Some(fetch_user()?); app_state_handle.user = Some(fetch_user()?);
// Setup capabilities
let endpoint = base_url.join("/api/v1/client/capability")?;
let header = generate_authorization_header();
let body = json!({
"capability": "cloudSaves",
"configuration": {}
});
let response = client
.post(endpoint)
.header("Authorization", header)
.json(&body)
.send()?;
if response.status().is_success() {
debug!("registered client for 'cloudSaves' capability")
}
} }
Ok(()) Ok(())
@ -184,11 +158,9 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
Url::parse(&db_lock.base_url.clone())? Url::parse(&db_lock.base_url.clone())?
}; };
let hostname = gethostname();
let endpoint = base_url.join("/api/v1/client/auth/initiate")?; let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
let body = InitiateRequestBody { let body = InitiateRequestBody {
name: format!("{} (Desktop)", hostname.into_string().unwrap()), name: "Drop Desktop Client".to_string(),
platform: env::consts::OS.to_string(), platform: env::consts::OS.to_string(),
}; };
@ -219,13 +191,9 @@ 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(_)) => { Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
let user = get_cached_object::<String, User>("user".to_owned()).unwrap();
return (AppStatus::Offline, Some(user));
}
Err(_) => return (AppStatus::SignedInNeedsReauth, None), Err(_) => return (AppStatus::SignedInNeedsReauth, None),
}; };
cache_object("user", &user_result).unwrap();
return (AppStatus::SignedIn, Some(user_result)); return (AppStatus::SignedIn, Some(user_result));
} }

View File

@ -1,71 +0,0 @@
use std::sync::RwLockReadGuard;
use crate::{
database::db::{borrow_db_checked, Database},
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 serde_binary::binary_stream::Endian;
#[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_binary::to_vec(data, Endian::Little).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> {
get_cached_object_db::<K, D>(key, &borrow_db_checked())
}
pub fn get_cached_object_db<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
key: K,
db: &Database,
) -> Result<D, RemoteAccessError> {
let bytes = cacache::read_sync(&db.cache_dir, key).map_err(|e| RemoteAccessError::Cache(e))?;
let data = serde_binary::from_slice::<D>(&bytes, Endian::Little).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,8 +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;
pub mod server_proto;

View File

@ -1,66 +0,0 @@
use std::{path::PathBuf, str::FromStr};
use http::{
uri::{Authority, PathAndQuery},
Request, Response, StatusCode, Uri,
};
use log::info;
use reqwest::blocking::Client;
use tauri::UriSchemeResponder;
use crate::database::db::borrow_db_checked;
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
let four_oh_four = Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Vec::new())
.unwrap();
responder.respond(four_oh_four);
}
pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
let db_handle = borrow_db_checked();
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
Some(e) => e,
None => return,
};
let remote_uri = db_handle.base_url.parse::<Uri>().unwrap();
let path = request.uri().path();
let mut new_uri = request.uri().clone().into_parts();
new_uri.path_and_query =
Some(PathAndQuery::from_str(&format!("{}?noWrapper=true", path)).unwrap());
new_uri.authority = remote_uri.authority().cloned();
new_uri.scheme = remote_uri.scheme().cloned();
let new_uri = Uri::from_parts(new_uri).unwrap();
let whitelist_prefix = vec!["/store", "/api", "/_", "/fonts"];
if whitelist_prefix
.iter()
.map(|f| !path.starts_with(f))
.all(|f| f)
{
webbrowser::open(&new_uri.to_string()).unwrap();
return;
}
let client = Client::new();
let response = client
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {}", web_token))
.headers(request.headers().clone())
.send()
.unwrap();
let response_status = response.status();
let response_body = response.bytes().unwrap();
let http_response = Response::builder()
.status(response_status)
.body(response_body.to_vec())
.unwrap();
responder.respond(http_response);
}

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2.0.0", "$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client", "productName": "Drop Desktop Client",
"version": "0.2.0-beta", "version": "0.2.1-beta",
"identifier": "dev.drop.app", "identifier": "dev.drop.app",
"build": { "build": {
"beforeDevCommand": "yarn dev --port 1432", "beforeDevCommand": "yarn dev --port 1432",

View File

@ -34,16 +34,10 @@ export type Game = {
mBannerId: string; mBannerId: string;
mCoverId: string; mCoverId: string;
mImageLibrary: string[]; mImageLibrary: string[];
mImageCarousel: string[];
};
export type GameVersion = {
launchCommandTemplate: string;
}; };
export enum AppStatus { export enum AppStatus {
NotConfigured = "NotConfigured", NotConfigured = "NotConfigured",
Offline = "Offline",
SignedOut = "SignedOut", SignedOut = "SignedOut",
SignedIn = "SignedIn", SignedIn = "SignedIn",
SignedInNeedsReauth = "SignedInNeedsReauth", SignedInNeedsReauth = "SignedInNeedsReauth",
@ -58,7 +52,7 @@ export enum GameStatusEnum {
Updating = "Updating", Updating = "Updating",
Uninstalling = "Uninstalling", Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired", SetupRequired = "SetupRequired",
Running = "Running", Running = "Running"
} }
export type GameStatus = { export type GameStatus = {
@ -70,17 +64,16 @@ export enum DownloadableType {
Game = "Game", Game = "Game",
Tool = "Tool", Tool = "Tool",
DLC = "DLC", DLC = "DLC",
Mod = "Mod", Mod = "Mod"
} }
export type DownloadableMetadata = { export type DownloadableMetadata = {
id: string; id: string,
version: string; version: string,
downloadType: DownloadableType; downloadType: DownloadableType
}; }
export type Settings = { export type Settings = {
autostart: boolean; autostart: boolean,
maxDownloadThreads: number; maxDownloadThreads: number,
forceOffline: boolean; }
};

259
yarn.lock
View File

@ -1399,10 +1399,10 @@
dependencies: dependencies:
"@tauri-apps/api" "^2.0.0" "@tauri-apps/api" "^2.0.0"
"@tauri-apps/plugin-shell@^2.2.1": "@tauri-apps/plugin-shell@>=2.0.0":
version "2.2.1" version "2.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.2.1.tgz#586ab725ef622ba65a946bff1a3e166cee181903" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz#b6fc88ab070fd5f620e46405715779aa44eb8428"
integrity sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA== integrity sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==
dependencies: dependencies:
"@tauri-apps/api" "^2.0.0" "@tauri-apps/api" "^2.0.0"
@ -1411,13 +1411,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/debug@^4.0.0":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917"
integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==
dependencies:
"@types/ms" "*"
"@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0": "@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0":
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
@ -1448,11 +1441,6 @@
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/ms@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
"@types/node@*": "@types/node@*":
version "22.7.4" version "22.7.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc"
@ -2080,11 +2068,6 @@ chalk@^5.3.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
character-entities@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
chokidar@^3.5.1, chokidar@^3.5.3, chokidar@^3.6.0: chokidar@^3.5.1, chokidar@^3.5.3, chokidar@^3.6.0:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@ -2475,20 +2458,13 @@ debug@^3.1.0, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.0.0, debug@^4.3.2: debug@^4.3.2:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies: dependencies:
ms "^2.1.3" ms "^2.1.3"
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
dependencies:
character-entities "^2.0.0"
deep-equal@~1.0.1: deep-equal@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -2547,11 +2523,6 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
destr@^2.0.3: destr@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
@ -2577,13 +2548,6 @@ devalue@^5.0.0:
resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.1.1.tgz#a71887ac0f354652851752654e4bd435a53891ae" resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.1.1.tgz#a71887ac0f354652851752654e4bd435a53891ae"
integrity sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw== integrity sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==
devlop@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
dependencies:
dequal "^2.0.0"
didyoumean@^1.2.2: didyoumean@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@ -3640,35 +3604,6 @@ koa@^2.14.2:
type-is "^1.6.16" type-is "^1.6.16"
vary "^1.1.2" vary "^1.1.2"
koa@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.16.1.tgz#ba1aae04d8319d7dac4a17a0d289d7482501e194"
integrity sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
cookies "~0.9.0"
debug "^4.3.2"
delegates "^1.0.0"
depd "^2.0.0"
destroy "^1.0.4"
encodeurl "^1.0.2"
escape-html "^1.0.3"
fresh "~0.5.2"
http-assert "^1.3.0"
http-errors "^1.6.3"
is-generator-function "^1.0.7"
koa-compose "^4.1.0"
koa-convert "^2.0.0"
on-finished "^2.3.0"
only "~0.0.2"
parseurl "^1.3.2"
statuses "^1.5.0"
type-is "^1.6.16"
vary "^1.1.2"
kolorist@^1.8.0: kolorist@^1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
@ -3877,190 +3812,6 @@ methods@^1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromark-core-commonmark@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz#6a45bbb139e126b3f8b361a10711ccc7c6e15e93"
integrity sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==
dependencies:
decode-named-character-reference "^1.0.0"
devlop "^1.0.0"
micromark-factory-destination "^2.0.0"
micromark-factory-label "^2.0.0"
micromark-factory-space "^2.0.0"
micromark-factory-title "^2.0.0"
micromark-factory-whitespace "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-chunked "^2.0.0"
micromark-util-classify-character "^2.0.0"
micromark-util-html-tag-name "^2.0.0"
micromark-util-normalize-identifier "^2.0.0"
micromark-util-resolve-all "^2.0.0"
micromark-util-subtokenize "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-destination@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639"
integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-label@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1"
integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==
dependencies:
devlop "^1.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-space@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc"
integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-title@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94"
integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==
dependencies:
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-whitespace@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1"
integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==
dependencies:
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-character@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6"
integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-chunked@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051"
integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-classify-character@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629"
integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-combine-extensions@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9"
integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==
dependencies:
micromark-util-chunked "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-decode-numeric-character-reference@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5"
integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-encode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8"
integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
micromark-util-html-tag-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825"
integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==
micromark-util-normalize-identifier@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d"
integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-resolve-all@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b"
integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==
dependencies:
micromark-util-types "^2.0.0"
micromark-util-sanitize-uri@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7"
integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-subtokenize@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.4.tgz#50d8ca981373c717f497dc64a0dbfccce6c03ed2"
integrity sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==
dependencies:
devlop "^1.0.0"
micromark-util-chunked "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-symbol@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8"
integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
micromark-util-types@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.1.tgz#a3edfda3022c6c6b55bfb049ef5b75d70af50709"
integrity sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==
micromark@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.1.tgz#294c2f12364759e5f9e925a767ae3dfde72223ff"
integrity sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==
dependencies:
"@types/debug" "^4.0.0"
debug "^4.0.0"
decode-named-character-reference "^1.0.0"
devlop "^1.0.0"
micromark-core-commonmark "^2.0.0"
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-chunked "^2.0.0"
micromark-util-combine-extensions "^2.0.0"
micromark-util-decode-numeric-character-reference "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-normalize-identifier "^2.0.0"
micromark-util-resolve-all "^2.0.0"
micromark-util-sanitize-uri "^2.0.0"
micromark-util-subtokenize "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"