Compare commits

..

2 Commits

Author SHA1 Message Date
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
101 changed files with 3762 additions and 10258 deletions

View File

@ -1,70 +0,0 @@
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-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-24.04-arm'
args: '--target aarch64-unknown-linux-gnu'
- 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 libgtk2.0-dev libsoup3.0-dev
# 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: 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 }}

9
.gitmodules vendored
View File

@ -1,9 +0,0 @@
[submodule "drop-base"]
path = drop-base
url = https://github.com/drop-oss/drop-base
[submodule "src-tauri/tailscale/libtailscale"]
path = src-tauri/tailscale/libtailscale
url = https://github.com/tailscale/libtailscale.git
[submodule "src-tauri/umu/umu-launcher"]
path = src-tauri/umu/umu-launcher
url = https://github.com/Open-Wine-Components/umu-launcher.git

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,78 +1,34 @@
<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" styles[props.status.type],
@click="() => buttonActions[props.status.type]()" showDropdown ? 'rounded-l-md' : 'rounded-md',
:class="[ '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',
styles[props.status.type], ]">
showDropdown ? 'rounded-l-md' : 'rounded-md', <component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }} {{ 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'
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group', ]">
'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="[ <TrashIcon class="size-5" />
active </button>
? '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" />
</button>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@ -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,23 +39,21 @@
<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>
</div> </div>
<WindowControl /> <WindowControl class="h-16 w-16 p-4" />
</div> </div>
</template> </template>
<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

@ -1,5 +1,5 @@
<template> <template>
<button class="transition h-full aspect-square text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-[1.1rem]"> <button class="transition h-10 w-10 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-2">
<slot /> <slot />
</button> </button>
</template> </template>

View File

@ -49,20 +49,17 @@
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"
:class="[ :class="[
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400', active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
'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,22 +80,27 @@ 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?.profilePictureObjectId ?? "" 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);
} }
const navigation: NavigationItem[] = [ const navigation: NavigationItem[] = [
{
label: "Account settings",
route: "/account",
prefix: "",
},
{ {
label: "App settings", label: "App settings",
route: "/settings", route: "/settings",
@ -108,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);
@ -136,16 +135,14 @@ async function auth() {
} }
function authWrapper_wrapper() { function authWrapper_wrapper() {
error.value = undefined;
loading.value = true; loading.value = true;
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;
}, 2000); }, 10000);
} }
async function continueManual() { async function continueManual() {

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.mIconObjectId);
}
}
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

@ -1,7 +1,4 @@
<template> <template>
<HeaderButton v-if="showMinimise" @click="() => minimise()">
<MinusIcon />
</HeaderButton>
<HeaderButton @click="() => close()"> <HeaderButton @click="() => close()">
<XMarkIcon /> <XMarkIcon />
</HeaderButton> </HeaderButton>
@ -11,14 +8,11 @@
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid"; import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
async function close(){
console.log(window);
const result = await window.close();
console.log(`closed window: ${result}`);
}
const window = getCurrentWindow(); const window = getCurrentWindow();
const showMinimise = await window.isMinimizable();
async function close() {
await window.close();
}
async function minimise() {
await window.minimize();
}
</script> </script>

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; gameId,
} = await invoke("fetch_game", { }
gameId, );
}); gameRegistry[gameId] = data.game;
gameRegistry[gameId] = { game: data.game, version: data.version };
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

@ -1,7 +1,7 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.3.0-rc-2", "version": "0.2.0-beta",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
@ -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",
@ -37,9 +35,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"sass-embedded": "^1.79.4", "sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13"
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

72
pages/account.vue Normal file
View File

@ -0,0 +1,72 @@
<template>
<div class="mx-auto max-w-7xl px-8">
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
Account
</h3>
</div>
<div class="mt-5">
<div class="divide-y divide-zinc-700">
<div class="py-6">
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Sign out of your Drop account on this device
</p>
</div>
<button
@click="signOut"
type="button"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
>
Sign out
</button>
</div>
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ error }}
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen } from '@tauri-apps/api/event'
import { useRouter } from '#imports'
import { XCircleIcon } from "@heroicons/vue/16/solid";
const router = useRouter()
const error = ref<string | null>(null)
// Listen for auth events
onMounted(async () => {
await listen('auth/signedout', () => {
router.push('/auth/signedout')
})
})
async function signOut() {
try {
error.value = null
await invoke('sign_out')
} catch (e) {
error.value = `Failed to sign out: ${e}`
}
}
</script>

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">
<NuxtLink
v-for="(nav, navIdx) in navigation"
:key="nav.route"
:class="[
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
navIdx === currentNavigationIndex
? 'bg-zinc-800 text-zinc-100'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="nav.route"
>
<div class="flex items-center w-full gap-x-3">
<img
class="size-6 flex-none object-cover bg-zinc-900 rounded"
:src="icons[navIdx]"
alt=""
/>
<p class="truncate text-sm font-display leading-6 flex-1">
{{ nav.label }}
</p>
</div>
</NuxtLink>
</ul>
</div> </div>
<div class="grow overflow-y-auto"> <div class="grow overflow-y-auto">
<NuxtErrorBoundary> <NuxtPage :libraryDownloadError = "libraryDownloadError" />
<NuxtPage />
<template #error="{ error }">
<main
class="grid min-h-full w-full place-items-center px-6 py-24 sm:py-32 lg:px-8"
>
<div class="text-center">
<p class="text-base font-semibold text-blue-600">Error</p>
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Failed to load library
</h1>
<p class="mt-6 text-base leading-7 text-zinc-400">
Drop couldn't load your library: "{{ error }}".
</p>
</div>
</main>
</template>
</NuxtErrorBoundary>
</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,153 +1,41 @@
<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" <h1
/> 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 }}
</h1>
<div <div
class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-zinc-900/80 to-transparent opacity-90" class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
/>
<div
class="absolute inset-0 bg-gradient-to-r from-zinc-900/95 via-zinc-900/80 to-transparent opacity-90"
/> />
</div> </div>
<!-- main page -->
<div class="relative z-10"> <div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<div class="px-8 pb-4"> <!-- game toolbar -->
<h1 <div class="h-full flex flex-row gap-x-4 items-stretch">
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8" <GameStatusButton
@install="() => installFlow()"
@launch="() => launch()"
@queue="() => queue()"
@uninstall="() => uninstall()"
@kill="() => kill()"
:status="status"
/>
<a
:href="remoteUrl"
target="_blank"
type="button"
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"
> >
{{ game.mName }} <BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
</h1>
<div class="flex flex-row gap-x-4 items-stretch mb-8"> Store
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 --> </a>
<GameStatusButton
@install="() => installFlow()"
@launch="() => launch()"
@queue="() => queue()"
@uninstall="() => uninstall()"
@kill="() => kill()"
@options="() => (configureModalOpen = true)"
:status="status"
/>
<a
:href="remoteUrl"
target="_blank"
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"
>
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store
</a>
</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>
</div> </div>
@ -156,9 +44,9 @@
<template #default> <template #default>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left"> <div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100" <DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
>Install {{ game.mName }}? >Install {{ game.mName }}?
</h3> </DialogTitle>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-zinc-400"> <p class="text-sm text-zinc-400">
Drop will add {{ game.mName }} to the queue to be downloaded. Drop will add {{ game.mName }} to the queue to be downloaded.
@ -350,7 +238,7 @@
<LoadingButton <LoadingButton
@click="() => install()" @click="() => install()"
:disabled=" :disabled="
!(versionOptions && versionOptions.length > 0) !(versionOptions && versionOptions.length > 0 && !installDir)
" "
:loading="installLoading" :loading="installLoading"
type="submit" type="submit"
@ -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();
@ -477,24 +290,13 @@ const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`, path: `/store/${game.value.id}`,
}); });
const bannerUrl = await useObject(game.value.mBannerObjectId); const bannerUrl = await useObject(game.value.mBannerId);
// Get all available images
const mediaUrls = await Promise.all(
game.value.mImageCarouselObjectIds.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

@ -158,7 +158,7 @@ function loadGamesForQueue(v: typeof queue.value) {
if (games.value[id]) return; if (games.value[id]) return;
(async () => { (async () => {
const gameData = await useGame(id); const gameData = await useGame(id);
const cover = await useObject(gameData.game.mCoverObjectId); const cover = await useObject(gameData.game.mCoverId);
games.value[id] = { ...gameData, cover }; games.value[id] = { ...gameData, cover };
})(); })();
} }
@ -167,7 +167,7 @@ function loadGamesForQueue(v: typeof queue.value) {
loadGamesForQueue(queue.value); loadGamesForQueue(queue.value);
async function onEnd(event: { oldIndex: number; newIndex: number }) { async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_download_in_queue", { await invoke("move_game_in_queue", {
oldIndex: event.oldIndex, oldIndex: event.oldIndex,
newIndex: event.newIndex, newIndex: event.newIndex,
}); });

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',
@ -45,7 +45,6 @@ import type { Component } from "vue";
import type { NavigationItem } from "~/types"; import type { NavigationItem } from "~/types";
import { platform } from '@tauri-apps/plugin-os'; import { platform } from '@tauri-apps/plugin-os';
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { UserIcon } from "@heroicons/vue/20/solid";
const systemData = await invoke<{ const systemData = await invoke<{
clientId: string; clientId: string;
@ -102,12 +101,6 @@ const navigation = computed(() => [
prefix: "/settings/downloads", prefix: "/settings/downloads",
icon: ArrowDownTrayIcon, icon: ArrowDownTrayIcon,
}, },
{
label: "Account",
route: "/settings/account",
prefix: "/settings/account",
icon: UserIcon
},
...(isDebugMode.value ? [{ ...(isDebugMode.value ? [{
label: "Debug Info", label: "Debug Info",
route: "/settings/debug", route: "/settings/debug",
@ -119,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,64 +0,0 @@
<template>
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
General
</h3>
</div>
<div class="mt-5 flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Sign out of your Drop account on this device
</p>
</div>
<button
@click="signOut"
type="button"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
>
Sign out
</button>
</div>
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ error }}
</h3>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useRouter } from "#imports";
import { XCircleIcon } from "@heroicons/vue/16/solid";
const router = useRouter();
const error = ref<string | null>(null);
// Listen for auth events
onMounted(async () => {
await listen("auth/signedout", () => {
router.push("/auth/signedout");
});
});
async function signOut() {
try {
error.value = null;
await invoke("sign_out");
} catch (e) {
error.value = `Failed to sign out: ${e}`;
}
}
</script>

View File

@ -1,15 +1,10 @@
<template> <template>
<div class="border-b border-zinc-700 py-5"> <div>
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100"> <div class="border-b border-zinc-600 py-2 px-1">
Downloads <div
</h3> class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
</div> >
<div class="ml-4 mt-2">
<div class="mt-5">
<div class="border-b border-zinc-600">
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div class="ml-4 mt-2 pb-4">
<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
</h3> </h3>
@ -20,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 }}
@ -38,12 +43,16 @@
</div> </div>
</div> </div>
<div class="flex shrink-0 items-center gap-x-6"> <div class="flex shrink-0 items-center gap-x-6">
<button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[ <button
dirs.length <= 1 @click="() => deleteDirectory(dirIdx)"
? 'text-zinc-700' :disabled="dirs.length <= 1"
: 'text-zinc-400 hover:text-zinc-100', :class="[
'-m-2.5 block p-2.5', dirs.length <= 1
]"> ? 'text-zinc-700'
: 'text-zinc-400 hover:text-zinc-100',
'-m-2.5 block p-2.5',
]"
>
<span class="sr-only">Open options</span> <span class="sr-only">Open options</span>
<TrashIcon class="size-5" aria-hidden="true" /> <TrashIcon class="size-5" aria-hidden="true" />
</button> </button>
@ -63,44 +72,37 @@
Maximum Download Threads Maximum Download Threads
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads" <input
@keypress="validateNumberInput" @paste="validatePaste" type="number"
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" /> name="threads"
id="threads"
min="1"
max="32"
v-model="downloadThreads"
@keypress="validateNumberInput"
@paste="validatePaste"
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div> </div>
<p class="mt-2 text-sm text-zinc-400"> <p class="mt-2 text-sm text-zinc-400">
The maximum number of concurrent download threads. Higher values may The maximum number of concurrent download threads. Higher values may
download faster but use more system resources. Default is 4. download faster but use more system resources. Default is 4.
</p> </p>
</div> </div>
<div class="mt-10 space-y-8">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Force Offline</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Drop will not make any external connections
</p>
</div>
<Switch v-model="forceOffline" :class="[
forceOffline ? 'bg-blue-600' : 'bg-zinc-700',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
]">
<span :class="[
forceOffline ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
]" />
</Switch>
</div>
</div>
<div class="mt-6"> <div class="mt-6">
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[ <button
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300', type="button"
saveState.success @click="saveDownloadThreads"
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600' :disabled="saveState.loading"
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600', :class="[
'disabled:bg-blue-600/50 disabled:cursor-not-allowed' 'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
]"> saveState.success
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
]"
>
{{ saveState.success ? 'Saved' : 'Save Changes' }} {{ saveState.success ? 'Saved' : 'Save Changes' }}
</button> </button>
</div> </div>
@ -108,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..."
}} }}
@ -141,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">
@ -185,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);
@ -197,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,
@ -260,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
@ -285,7 +318,7 @@ async function saveSettings() {
function validateNumberInput(event: KeyboardEvent) { function validateNumberInput(event: KeyboardEvent) {
// Allow only numbers and basic control keys // Allow only numbers and basic control keys
if (!/^\d$/.test(event.key) && if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault(); event.preventDefault();
} }
} }

View File

@ -1,59 +1,60 @@
<template> <template>
<div class="border-b border-zinc-700 py-5"> <div class="divide-y divide-zinc-700">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100"> <div class="py-6">
General <h2 class="text-base font-semibold font-display leading-7 text-zinc-100">General</h2>
</h3> <p class="mt-1 text-sm leading-6 text-zinc-400">
</div> Configure basic application settings
</p>
<div class="mt-5 space-y-8"> <div class="mt-10 space-y-8">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div> <div>
<h3 class="text-sm font-medium leading-6 text-zinc-100"> <h3 class="text-sm font-medium leading-6 text-zinc-100">Start with system</h3>
Start with system <p class="mt-1 text-sm leading-6 text-zinc-400">
</h3> Drop will automatically start when you log into your computer
<p class="mt-1 text-sm leading-6 text-zinc-400"> </p>
Drop will automatically start when you log into your computer </div>
</p> <Switch
</div> v-model="autostartEnabled"
<Switch :class="[
v-model="autostartEnabled" autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700',
:class="[ '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'
autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700', ]"
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out', >
]" <span
> :class="[
<span autostartEnabled ? 'translate-x-5' : 'translate-x-0',
:class="[ 'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
autostartEnabled ? 'translate-x-5' : 'translate-x-0', ]"
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out', />
]" </Switch>
/> </div>
</Switch> </div>
</div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Switch } from "@headlessui/vue"; import { Switch } from '@headlessui/vue'
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
defineProps<{}>(); defineProps<{}>()
const autostartEnabled = ref<boolean>(false); const autostartEnabled = ref<boolean>(false)
// Load initial state // Load initial state
invoke("get_autostart_enabled").then((enabled) => { invoke('get_autostart_enabled').then((enabled) => {
autostartEnabled.value = enabled as boolean; autostartEnabled.value = enabled as boolean
}); })
// Watch for changes and update autostart // Watch for changes and update autostart
watch(autostartEnabled, async (newValue: boolean) => { watch(autostartEnabled, async (newValue: boolean) => {
try { try {
await invoke("toggle_autostart", { enabled: newValue }); await invoke('toggle_autostart', { enabled: newValue })
} catch (error) { } catch (error) {
console.error("Failed to toggle autostart:", error); console.error('Failed to toggle autostart:', error)
// Revert the toggle if it failed // Revert the toggle if it failed
autostartEnabled.value = !newValue; autostartEnabled.value = !newValue
} }
}); })
</script> </script>

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`);
}); });
*/
}); });

3460
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.3.0-rc-2" version = "0.2.0-beta-prerelease-1"
description = "The client application for the open-source, self-hosted game distribution platform Drop" description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"] authors = ["Drop OSS"]
edition = "2021" edition = "2021"
@ -16,6 +16,8 @@ tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib" name = "drop_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build]
rustflags = ["-C", "target-feature=+aes,+sse2"] rustflags = ["-C", "target-feature=+aes,+sse2"]
@ -23,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"
@ -47,36 +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"
http-serde = "2.1.1"
reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1"
deranged = "=0.4.0"
droplet-rs = "0.7.3"
gethostname = "1.0.1"
zstd = "0.13.3"
tar = "0.4.44"
rand = "0.9.1"
regex = "1.11.1"
tempfile = "3.19.1"
schemars = "0.8.22"
sha1 = "0.10.6"
dirs = "6.0.0"
whoami = "1.6.0"
filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
# tailscale = { path = "./tailscale" }
[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"]
@ -91,16 +70,23 @@ 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"
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc" features = [] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.12" version = "0.12"
default-features = false features = ["json", "blocking"]
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"

View File

@ -1,3 +0,0 @@
pub mod autostart;
pub mod cleanup;
pub mod commands;

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,262 +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();
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

@ -6,12 +6,14 @@ use std::{
use serde_json::Value; use serde_json::Value;
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError}; use crate::{
database::{db::borrow_db_mut_checked, settings::Settings},
download_manager::internal_error::InternalError,
};
use super::{ use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings,
}; };
// Will, in future, return disk/remaining size // Will, in future, return disk/remaining size
@ -31,7 +33,7 @@ pub fn delete_download_dir(index: usize) {
} }
#[tauri::command] #[tauri::command]
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> { pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
// Check the new directory is all good // Check the new directory is all good
let new_dir_path = Path::new(&new_dir); let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() { if new_dir_path.exists() {
@ -72,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

@ -1,42 +1,138 @@
use std::{ use std::{
collections::HashMap,
fs::{self, create_dir_all}, fs::{self, create_dir_all},
path::PathBuf, hash::Hash,
path::{Path, PathBuf},
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
use chrono::Utc; use chrono::Utc;
use log::{debug, error, info, warn}; use directories::BaseDirs;
use native_model::{Decode, Encode}; use log::{debug, error, info};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::serde_as;
use tauri::AppHandle;
use url::Url; use url::Url;
use crate::DB; use crate::{
database::settings::Settings,
download_manager::downloadable_metadata::DownloadableMetadata,
games::{library::push_game_update, state::GameStatusManager},
process::process_manager::Platform,
DB,
};
use super::models::data::Database; #[derive(serde::Serialize, Clone, Deserialize)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
pub setup_command: String,
pub setup_args: Vec<String>,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
}
impl Database {
fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings {
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)]
pub struct DropDatabaseSerializer; pub struct DropDatabaseSerializer;
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T> impl<T: Serialize + DeserializeOwned> DeSerializer<T> for DropDatabaseSerializer {
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string())) serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string()))
} }
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> { fn deserialize<R: std::io::Read>(&self, s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new(); serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string()))
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val =
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
} }
} }
@ -54,25 +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");
let pfx_dir = data_root_dir.join("pfx");
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();
create_dir_all(&pfx_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()
@ -93,14 +185,26 @@ 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> {
warn!("{}", _e);
let new_path = { let new_path = {
let time = Utc::now().timestamp(); let time = Utc::now().timestamp();
let mut base = db_path.clone(); let mut base = db_path.clone();
@ -116,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

@ -1,4 +1,4 @@
pub mod commands; pub mod commands;
pub mod db; pub mod db;
pub mod debug; pub mod debug;
pub mod models; pub mod settings;

View File

@ -1,240 +0,0 @@
pub mod data {
use native_model::{native_model, Model};
use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion;
pub type Database = v2::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
pub mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use super::*;
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 4, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool, // ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
#[derive(serde::Serialize, Clone, Deserialize)]
#[native_model(id = 6, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
#[native_model(id = 8, version = 1)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}
#[native_model(id = 1, version = 1)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
}
pub mod v2 {
use std::{collections::HashMap, path::PathBuf, process::Command};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use super::*;
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
umu_installed: bool,
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
}
}

View File

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
}
}
}
// Ideally use pointers instead of a macro to assign the settings
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
// where T: for<'a> Deserialize<'a>
// {
// *t = serde_json::from_value(v)?;
// Ok(())
// }

View File

@ -1,6 +1,6 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{database::models::data::DownloadableMetadata, AppState}; use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
#[tauri::command] #[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {

View File

@ -12,13 +12,12 @@ use std::{
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::error::application_download_error::ApplicationDownloadError;
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{ use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue, download_manager_builder::{CurrentProgressObject, DownloadAgent},
downloadable_metadata::DownloadableMetadata,
queue::Queue,
}; };
pub enum DownloadManagerSignal { pub enum DownloadManagerSignal {
@ -49,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,
@ -168,7 +167,6 @@ impl DownloadManager {
self.command_sender self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue) .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap(); .unwrap();
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
} }
pub fn pause_downloads(&self) { pub fn pause_downloads(&self) {
self.command_sender self.command_sender

View File

@ -11,14 +11,17 @@ use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use crate::{ use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError, error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
}; };
use super::{ use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
downloadable::Downloadable,
downloadable_metadata::DownloadableMetadata,
progress_object::ProgressObject,
queue::Queue,
}; };
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>; pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
@ -206,15 +209,11 @@ impl DownloadManagerBuilder {
} }
if self.current_download_agent.is_some() { if self.current_download_agent.is_some() {
if self.download_queue.read().front().unwrap() debug!(
== &self.current_download_agent.as_ref().unwrap().metadata() "Current download agent: {:?}",
{ self.current_download_agent.as_ref().unwrap().metadata()
debug!( );
"Current download agent: {:?}", return;
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
} }
debug!("current download queue: {:?}", self.download_queue.read()); debug!("current download queue: {:?}", self.download_queue.read());
@ -254,7 +253,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();
} }
} }
@ -286,7 +285,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

@ -2,13 +2,11 @@ use std::sync::Arc;
use tauri::AppHandle; use tauri::AppHandle;
use crate::{ use crate::error::application_download_error::ApplicationDownloadError;
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{ use super::{
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject}, download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
}; };
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
@ -18,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

@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}

View File

@ -0,0 +1,27 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum InternalError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for InternalError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InternalError::IOError(error) => write!(f, "{}", error),
InternalError::SignalError(send_error) => write!(f, "{}", send_error),
}
}
}
impl<T> From<SendError<T>> for InternalError<T> {
fn from(value: SendError<T>) -> Self {
InternalError::SignalError(value)
}
}
impl<T> From<io::Error> for InternalError<T> {
fn from(value: io::Error) -> Self {
InternalError::IOError(value)
}
}

View File

@ -1,5 +1,10 @@
pub mod commands; pub mod commands;
pub mod download_manager; pub mod download_manager;
pub mod download_manager_builder; pub mod download_manager_builder;
pub mod download_thread_control_flag;
pub mod downloadable; pub mod downloadable;
pub mod util; pub mod downloadable_metadata;
pub mod internal_error;
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;

View File

@ -10,10 +10,8 @@ use std::{
use atomic_instant_full::AtomicInstant; use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle; use throttle_my_fn::throttle;
use crate::download_manager::download_manager::DownloadManagerSignal;
use super::{ use super::{
rolling_progress_updates::RollingProgressWindow, download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
}; };
#[derive(Clone)] #[derive(Clone)]

View File

@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use crate::database::models::data::DownloadableMetadata; use super::downloadable_metadata::DownloadableMetadata;
#[derive(Clone)] #[derive(Clone)]
pub struct Queue { pub struct Queue {

View File

@ -1,4 +0,0 @@
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;
pub mod download_thread_control_flag;

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

@ -1,27 +0,0 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum DownloadManagerError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for DownloadManagerError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadManagerError::IOError(error) => write!(f, "{}", error),
DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
}
}
}
impl<T> From<SendError<T>> for DownloadManagerError<T> {
fn from(value: SendError<T>) -> Self {
DownloadManagerError::SignalError(value)
}
}
impl<T> From<io::Error> for DownloadManagerError<T> {
fn from(value: io::Error) -> Self {
DownloadManagerError::IOError(value)
}
}

View File

@ -1,8 +1,6 @@
pub mod application_download_error; pub mod application_download_error;
pub mod drop_server_error; pub mod drop_server_error;
pub mod download_manager_error;
pub mod library_error; 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

@ -10,45 +10,39 @@ 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),
} }
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() { f,
return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again."); "{}: {}",
} error,
error
write!( .source()
f, .map(|e| e.to_string())
"{}: {}", .or_else(|| Some("Unknown error".to_string()))
error, .unwrap()
error ),
.source()
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.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!(
@ -57,7 +51,7 @@ impl Display for RemoteAccessError {
status, response status, response
), ),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"), RemoteAccessError::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::Cache(error) => write!(f, "Cache Error: {}", error), RemoteAccessError::Generic(message) => write!(f, "{}", message),
} }
} }
} }

View File

@ -1,23 +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,110 +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 collection;
pub mod commands;

View File

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

@ -3,7 +3,9 @@ use std::sync::{Arc, Mutex};
use crate::{ use crate::{
download_manager::{ download_manager::{
download_manager::DownloadManagerSignal, downloadable::Downloadable, download_manager::DownloadManagerSignal, downloadable::Downloadable,
}, error::download_manager_error::DownloadManagerError, AppState internal_error::InternalError,
},
AppState,
}; };
use super::download_agent::GameDownloadAgent; use super::download_agent::GameDownloadAgent;
@ -14,7 +16,7 @@ pub fn download_game(
game_version: String, game_version: String,
install_dir: usize, install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> { ) -> Result<(), InternalError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender(); let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id, game_id,

View File

@ -1,12 +1,15 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::database::db::borrow_db_checked; use crate::database::db::{
use crate::database::models::data::{ borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus, GameDownloadStatus,
}; };
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::downloadable::Downloadable; use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject}; use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError; use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
@ -22,6 +25,7 @@ use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use urlencoding::encode;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use rustix::fs::{fallocate, FallocateFlags}; use rustix::fs::{fallocate, FallocateFlags};
@ -95,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 {
@ -132,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()))?;
@ -247,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() {
@ -278,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 |_| {
@ -364,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())
@ -372,11 +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 db_handle.applications.transient_statuses.remove(meta);
.applications });
.transient_statuses
.remove(&self.metadata());
} }
fn on_complete(&self, app_handle: &tauri::AppHandle) { fn on_complete(&self, app_handle: &tauri::AppHandle) {
@ -398,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

@ -1,5 +1,7 @@
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; use crate::download_manager::download_thread_control_flag::{
use crate::download_manager::util::progress_object::ProgressHandle; DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError; use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext; use crate::games::downloads::manifest::DropDownloadContext;

View File

@ -5,29 +5,25 @@ use std::thread::spawn;
use log::{debug, error, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::Emitter; use tauri::Emitter;
use tauri::AppHandle; use tauri::{AppHandle, Manager};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
use crate::database::models::data::{ use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::download_manager::DownloadStatus;
use crate::error::library_error::LibraryError; use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header; use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::{cache_object, get_cached_object, 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,
@ -36,11 +32,10 @@ pub struct Game {
m_description: String, m_description: String,
// mDevelopers // mDevelopers
// mPublishers // mPublishers
m_icon_object_id: String, m_icon_id: String,
m_banner_object_id: String, m_banner_id: String,
m_cover_object_id: String, m_cover_id: String,
m_image_library_object_ids: Vec<String>, m_image_library: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
} }
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent { pub struct GameUpdateEvent {
@ -49,7 +44,6 @@ pub struct GameUpdateEvent {
Option<GameDownloadStatus>, Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>, Option<ApplicationTransientStatus>,
), ),
pub version: Option<GameVersion>,
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@ -72,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();
@ -89,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();
@ -105,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);
@ -169,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();
@ -208,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>>,
@ -282,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
@ -293,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 {})),
); );
@ -303,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,
@ -320,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();
@ -331,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
@ -344,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),
); );
} }
@ -372,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();
@ -380,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()),
@ -389,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
@ -397,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
@ -406,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,
@ -431,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();
@ -439,68 +340,14 @@ pub fn on_game_complete(
Ok(()) Ok(())
} }
pub fn push_game_update( pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
app_handle: &AppHandle,
game_id: &String,
version: Option<GameVersion>,
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

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

View File

@ -1,7 +1,4 @@
use crate::database::{ use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
db::borrow_db_checked,
models::data::{ApplicationTransientStatus, GameDownloadStatus},
};
pub type GameStatusWithTransient = ( pub type GameStatusWithTransient = (
Option<GameDownloadStatus>, Option<GameDownloadStatus>,

View File

@ -1,38 +1,39 @@
#![feature(try_trait_v2)]
mod database; mod database;
mod games; mod games;
mod client; mod autostart;
mod cleanup;
mod commands;
mod download_manager; mod download_manager;
mod error; mod error;
mod process; mod process;
mod remote; mod remote;
use crate::database::db::DatabaseImpls; use crate::database::db::DatabaseImpls;
use client::{ use autostart::{get_autostart_enabled, toggle_autostart};
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart}, use cleanup::{cleanup_and_exit, quit};
cleanup::{cleanup_and_exit, quit}, use commands::fetch_state;
};
use client::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,
fetch_system_data, update_settings, fetch_system_data, update_settings,
}; };
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR}; use database::db::{
use database::models::data::GameDownloadStatus; borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
};
use download_manager::commands::{ use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads, cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
}; };
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::{header::*, response::Builder as ResponseBuilder};
use log::{debug, info, warn, LevelFilter}; use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
@ -41,13 +42,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::server_proto::{handle_server_proto, handle_server_proto_offline};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::env; use std::env;
use std::path::Path; use std::path::Path;
@ -57,16 +56,16 @@ use std::{
collections::HashMap, collections::HashMap,
sync::{LazyLock, Mutex}, sync::{LazyLock, Mutex},
}; };
use tauri::ipc::IpcResponse;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder; use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; 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,
@ -81,7 +80,7 @@ pub struct User {
username: String, username: String,
admin: bool, admin: bool,
display_name: String, display_name: String,
profile_picture_object_id: String, profile_picture: String,
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@ -155,8 +154,8 @@ fn setup(handle: AppHandle) -> AppState<'static> {
drop(db_handle); drop(db_handle);
for (game_id, status) in statuses.into_iter() { for (game_id, status) in statuses.into_iter() {
match status { match status {
GameDownloadStatus::Remote {} => {} database::db::GameDownloadStatus::Remote {} => {}
GameDownloadStatus::SetupRequired { database::db::GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -165,7 +164,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id); missing_games.push(game_id);
} }
} }
GameDownloadStatus::Installed { database::db::GameDownloadStatus::Installed {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -193,7 +192,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
debug!("finished setup!"); debug!("finished setup!");
// Sync autostart state // Sync autostart state
if let Err(e) = sync_autostart_on_startup(&handle) { if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
warn!("failed to sync autostart state: {}", e); warn!("failed to sync autostart state: {}", e);
} }
@ -240,7 +239,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,
@ -249,14 +247,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,
@ -282,9 +272,10 @@ pub fn run() {
debug!("initialized drop client"); debug!("initialized drop client");
app.manage(Mutex::new(state)); app.manage(Mutex::new(state));
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{ {
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
let _ = app.deep_link().register_all(); app.deep_link().register_all()?;
debug!("registered all pre-defined deep links"); debug!("registered all pre-defined deep links");
} }
@ -327,25 +318,23 @@ pub fn run() {
], ],
)?; )?;
run_on_tray(|| { TrayIconBuilder::new()
TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone())
.icon(app.default_window_icon().unwrap().clone()) .menu(&menu)
.menu(&menu) .on_menu_event(|app, event| match event.id.as_ref() {
.on_menu_event(|app, event| match event.id.as_ref() { "open" => {
"open" => { app.webview_windows().get("main").unwrap().show().unwrap();
app.webview_windows().get("main").unwrap().show().unwrap(); }
} "quit" => {
"quit" => { cleanup_and_exit(app, &app.state());
cleanup_and_exit(app, &app.state()); }
}
_ => { _ => {
warn!("menu event not handled: {:?}", event.id); println!("menu event not handled: {:?}", event.id);
} }
}) })
.build(app) .build(app)
.expect("error while setting up tray menu"); .expect("error while setting up tray menu");
});
{ {
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
@ -370,32 +359,40 @@ 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, let header = generate_authorization_header();
fetch_object_offline, let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
request, let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
responder f.header("Authorization", header)
); })
}) .unwrap()
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| { .send();
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state(); if response.is_err() {
offline!( warn!(
state, "failed to fetch object with error: {}",
handle_server_proto, response.err().unwrap()
handle_server_proto_offline, );
request, responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
responder 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 {
run_on_tray(|| { window.hide().unwrap();
window.hide().unwrap(); api.prevent_close();
api.prevent_close();
});
} }
}) })
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -403,20 +400,9 @@ pub fn run() {
app.run(|_app_handle, event| { app.run(|_app_handle, event| {
if let RunEvent::ExitRequested { code, api, .. } = event { if let RunEvent::ExitRequested { code, api, .. } = event {
run_on_tray(|| { if code.is_none() {
if code.is_none() { api.prevent_exit();
api.prevent_exit(); }
}
});
} }
}); });
} }
fn run_on_tray<T: FnOnce() -> ()>(f: T) {
if match std::env::var("NO_TRAY_ICON") {
Ok(s) => s.to_lowercase() != "true",
Err(_) => true,
} {
(f)();
}
}

View File

@ -1 +1,13 @@
// Since this code isn't being used, we can either:
// 1. Delete the entire file if compatibility features are not planned
// 2. Or add a TODO comment if planning to implement later
// Option 1: Delete the file
// Delete src-tauri/src/process/compat.rs
// Option 2: Add TODO comment
/*
TODO: Compatibility layer for running Windows games on Linux
This module is currently unused but reserved for future implementation
of Windows game compatibility features on Linux.
*/

View File

@ -1,4 +1,3 @@
pub mod commands; pub mod commands;
#[cfg(target_os = "linux")]
pub mod compat; pub mod compat;
pub mod process_manager; pub mod process_manager;

View File

@ -1,29 +1,24 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::OpenOptions, fs::{File, OpenOptions},
io::{self}, io::{self, Error},
path::PathBuf, path::{Path, PathBuf},
process::{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;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{ use crate::{
database::{ database::db::{
db::{borrow_db_mut_checked, DATA_ROOT_DIR}, borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR
models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
GameVersion,
},
}, },
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError, error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager}, games::{library::push_game_update, state::GameStatusManager},
AppState, DB, AppState, DB,
@ -44,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(),
@ -66,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),
@ -78,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) => {
@ -135,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
} }
@ -196,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
@ -204,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)
@ -239,58 +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: _, &meta,
install_dir: _, command.to_string_lossy().to_string(),
} => (&game_version.launch_command, &game_version.launch_args), game_version,
GameDownloadStatus::SetupRequired { target_current_dir,
version_name: _, log_file,
install_dir: _, error_file,
} => (&game_version.setup_command, &game_version.setup_args), )
GameDownloadStatus::Remote {} => unreachable!("nuh uh"), .map_err(ProcessError::IOError)?;
};
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,
launch.to_string(),
args.to_vec(),
game_version,
install_dir,
);
let launch_string = SimpleCurlyFormat
.format(&game_version.launch_command_template, &[launch_string])
.map_err(|e| ProcessError::FormatError(e.to_string()))?
.to_string();
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
let mut command = Command::new("cmd");
#[cfg(target_os = "windows")]
command.raw_arg(format!("/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
@ -300,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 {})),
); );
@ -328,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, log_file: File,
) -> String { error_file: File,
format!("\"{}\" {}", launch_command, args.join(" ")) ) -> 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()
} }
} }
pub 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,37 +1,27 @@
use std::{collections::HashMap, env}; 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 tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::{ database::db::{
db::{borrow_db_checked, borrow_db_mut_checked, save_db}, borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
models::data::DatabaseAuth,
}, },
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppStatus, User, AppState, AppStatus, User, DB,
}; };
use super::{ use super::requests::make_request;
cache::{cache_object, get_cached_object},
requests::make_request,
};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CapabilityConfiguration {}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct InitiateRequestBody { struct InitiateRequestBody {
name: String, name: String,
platform: String, platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -49,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();
@ -63,6 +67,8 @@ pub fn generate_authorization_header() -> String {
} }
pub fn fetch_user() -> Result<User, RemoteAccessError> { pub fn fetch_user() -> Result<User, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
@ -109,9 +115,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?; let response = client.post(endpoint).json(&body).send()?;
debug!("handshake responsded with {}", response.status().as_u16()); debug!("handshake responsded with {}", response.status().as_u16());
if !response.status().is_success() {
return Err(RemoteAccessError::InvalidResponse(response.json()?));
}
let response_struct: HandshakeResponse = response.json()?; let response_struct: HandshakeResponse = response.json()?;
{ {
@ -120,28 +123,17 @@ 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 app_state = app.state::<Mutex<AppState>>();
let token = client let mut app_state_handle = app_state.lock().unwrap();
.post(base_url.join("/api/v1/client/user/webtoken").unwrap()) app_state_handle.status = AppStatus::SignedIn;
.header("Authorization", header) app_state_handle.user = Some(fetch_user()?);
.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();
Ok(()) Ok(())
} }
@ -166,16 +158,10 @@ 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(),
capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
}; };
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
@ -205,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,68 +0,0 @@
use crate::{
database::{db::borrow_db_checked, models::data::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,20 +1,16 @@
use std::sync::Mutex; use std::sync::Mutex;
use log::debug;
use reqwest::blocking::Client;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus, AppState, AppStatus,
}; };
use super::{ use super::{
auth::{auth_initiate_logic, recieve_handshake, setup}, auth::{auth_initiate_logic, recieve_handshake, setup},
cache::{cache_object, get_cached_object},
remote::use_remote_logic, remote::use_remote_logic,
}; };
@ -39,26 +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,53 +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,65 +0,0 @@
use std::str::FromStr;
use http::{
uri::PathAndQuery,
Request, Response, StatusCode, Uri,
};
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 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View File

@ -1,294 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "abs-file-macro"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3909701959b9fcb4d065c3cf3044fe77f6b07a85d748be4630f5214e8d7acc1"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "prettyplease"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tailscale"
version = "0.1.0"
dependencies = [
"abs-file-macro",
"bindgen",
"libc",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

View File

@ -1,11 +0,0 @@
[package]
name = "tailscale"
version = "0.1.0"
edition = "2024"
[build-dependencies]
bindgen = "*"
abs-file-macro = "0.1.2"
[dependencies]
libc = "0.2.172"

View File

@ -1,36 +0,0 @@
extern crate bindgen;
use abs_file_macro::abs_file;
use std::path::PathBuf;
use std::process::Command;
fn main() {
let build_folder = PathBuf::from(abs_file!());
let build_folder = build_folder.parent().unwrap();
let in_path = build_folder.join("libtailscale");
let out_path = build_folder.join("src/");
let mut make_cmd = Command::new("make");
make_cmd.arg("c-archive");
make_cmd.current_dir(in_path.clone());
make_cmd.status().expect("Make build failed");
let bindings = bindgen::Builder::default()
.header(in_path.join("libtailscale.h").to_str().unwrap())
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
println!("cargo:rerun-if-changed=libtailscale/tailscale.go");
println!(
"cargo:rustc-link-search=native={}",
in_path.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static={}", "tailscale");
}

View File

@ -1,363 +0,0 @@
/* automatically generated by rust-bindgen 0.71.1 */
#[derive(PartialEq, Copy, Clone, Hash, Debug, Default)]
#[repr(C)]
pub struct __BindgenComplex<T> {
pub re: T,
pub im: T,
}
pub const _ERRNO_H: u32 = 1;
pub const _FEATURES_H: u32 = 1;
pub const _DEFAULT_SOURCE: u32 = 1;
pub const __GLIBC_USE_ISOC2Y: u32 = 0;
pub const __GLIBC_USE_ISOC23: u32 = 0;
pub const __USE_ISOC11: u32 = 1;
pub const __USE_ISOC99: u32 = 1;
pub const __USE_ISOC95: u32 = 1;
pub const __USE_POSIX_IMPLICITLY: u32 = 1;
pub const _POSIX_SOURCE: u32 = 1;
pub const _POSIX_C_SOURCE: u32 = 200809;
pub const __USE_POSIX: u32 = 1;
pub const __USE_POSIX2: u32 = 1;
pub const __USE_POSIX199309: u32 = 1;
pub const __USE_POSIX199506: u32 = 1;
pub const __USE_XOPEN2K: u32 = 1;
pub const __USE_XOPEN2K8: u32 = 1;
pub const _ATFILE_SOURCE: u32 = 1;
pub const __WORDSIZE: u32 = 64;
pub const __WORDSIZE_TIME64_COMPAT32: u32 = 0;
pub const __TIMESIZE: u32 = 64;
pub const __USE_TIME_BITS64: u32 = 1;
pub const __USE_MISC: u32 = 1;
pub const __USE_ATFILE: u32 = 1;
pub const __USE_FORTIFY_LEVEL: u32 = 0;
pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0;
pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0;
pub const __GLIBC_USE_C23_STRTOL: u32 = 0;
pub const _STDC_PREDEF_H: u32 = 1;
pub const __STDC_IEC_559__: u32 = 1;
pub const __STDC_IEC_60559_BFP__: u32 = 201404;
pub const __STDC_IEC_559_COMPLEX__: u32 = 1;
pub const __STDC_IEC_60559_COMPLEX__: u32 = 201404;
pub const __STDC_ISO_10646__: u32 = 201706;
pub const __GNU_LIBRARY__: u32 = 6;
pub const __GLIBC__: u32 = 2;
pub const __GLIBC_MINOR__: u32 = 41;
pub const _SYS_CDEFS_H: u32 = 1;
pub const __glibc_c99_flexarr_available: u32 = 1;
pub const __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI: u32 = 0;
pub const __HAVE_GENERIC_SELECTION: u32 = 1;
pub const _BITS_ERRNO_H: u32 = 1;
pub const EPERM: u32 = 1;
pub const ENOENT: u32 = 2;
pub const ESRCH: u32 = 3;
pub const EINTR: u32 = 4;
pub const EIO: u32 = 5;
pub const ENXIO: u32 = 6;
pub const E2BIG: u32 = 7;
pub const ENOEXEC: u32 = 8;
pub const EBADF: u32 = 9;
pub const ECHILD: u32 = 10;
pub const EAGAIN: u32 = 11;
pub const ENOMEM: u32 = 12;
pub const EACCES: u32 = 13;
pub const EFAULT: u32 = 14;
pub const ENOTBLK: u32 = 15;
pub const EBUSY: u32 = 16;
pub const EEXIST: u32 = 17;
pub const EXDEV: u32 = 18;
pub const ENODEV: u32 = 19;
pub const ENOTDIR: u32 = 20;
pub const EISDIR: u32 = 21;
pub const EINVAL: u32 = 22;
pub const ENFILE: u32 = 23;
pub const EMFILE: u32 = 24;
pub const ENOTTY: u32 = 25;
pub const ETXTBSY: u32 = 26;
pub const EFBIG: u32 = 27;
pub const ENOSPC: u32 = 28;
pub const ESPIPE: u32 = 29;
pub const EROFS: u32 = 30;
pub const EMLINK: u32 = 31;
pub const EPIPE: u32 = 32;
pub const EDOM: u32 = 33;
pub const ERANGE: u32 = 34;
pub const EDEADLK: u32 = 35;
pub const ENAMETOOLONG: u32 = 36;
pub const ENOLCK: u32 = 37;
pub const ENOSYS: u32 = 38;
pub const ENOTEMPTY: u32 = 39;
pub const ELOOP: u32 = 40;
pub const EWOULDBLOCK: u32 = 11;
pub const ENOMSG: u32 = 42;
pub const EIDRM: u32 = 43;
pub const ECHRNG: u32 = 44;
pub const EL2NSYNC: u32 = 45;
pub const EL3HLT: u32 = 46;
pub const EL3RST: u32 = 47;
pub const ELNRNG: u32 = 48;
pub const EUNATCH: u32 = 49;
pub const ENOCSI: u32 = 50;
pub const EL2HLT: u32 = 51;
pub const EBADE: u32 = 52;
pub const EBADR: u32 = 53;
pub const EXFULL: u32 = 54;
pub const ENOANO: u32 = 55;
pub const EBADRQC: u32 = 56;
pub const EBADSLT: u32 = 57;
pub const EDEADLOCK: u32 = 35;
pub const EBFONT: u32 = 59;
pub const ENOSTR: u32 = 60;
pub const ENODATA: u32 = 61;
pub const ETIME: u32 = 62;
pub const ENOSR: u32 = 63;
pub const ENONET: u32 = 64;
pub const ENOPKG: u32 = 65;
pub const EREMOTE: u32 = 66;
pub const ENOLINK: u32 = 67;
pub const EADV: u32 = 68;
pub const ESRMNT: u32 = 69;
pub const ECOMM: u32 = 70;
pub const EPROTO: u32 = 71;
pub const EMULTIHOP: u32 = 72;
pub const EDOTDOT: u32 = 73;
pub const EBADMSG: u32 = 74;
pub const EOVERFLOW: u32 = 75;
pub const ENOTUNIQ: u32 = 76;
pub const EBADFD: u32 = 77;
pub const EREMCHG: u32 = 78;
pub const ELIBACC: u32 = 79;
pub const ELIBBAD: u32 = 80;
pub const ELIBSCN: u32 = 81;
pub const ELIBMAX: u32 = 82;
pub const ELIBEXEC: u32 = 83;
pub const EILSEQ: u32 = 84;
pub const ERESTART: u32 = 85;
pub const ESTRPIPE: u32 = 86;
pub const EUSERS: u32 = 87;
pub const ENOTSOCK: u32 = 88;
pub const EDESTADDRREQ: u32 = 89;
pub const EMSGSIZE: u32 = 90;
pub const EPROTOTYPE: u32 = 91;
pub const ENOPROTOOPT: u32 = 92;
pub const EPROTONOSUPPORT: u32 = 93;
pub const ESOCKTNOSUPPORT: u32 = 94;
pub const EOPNOTSUPP: u32 = 95;
pub const EPFNOSUPPORT: u32 = 96;
pub const EAFNOSUPPORT: u32 = 97;
pub const EADDRINUSE: u32 = 98;
pub const EADDRNOTAVAIL: u32 = 99;
pub const ENETDOWN: u32 = 100;
pub const ENETUNREACH: u32 = 101;
pub const ENETRESET: u32 = 102;
pub const ECONNABORTED: u32 = 103;
pub const ECONNRESET: u32 = 104;
pub const ENOBUFS: u32 = 105;
pub const EISCONN: u32 = 106;
pub const ENOTCONN: u32 = 107;
pub const ESHUTDOWN: u32 = 108;
pub const ETOOMANYREFS: u32 = 109;
pub const ETIMEDOUT: u32 = 110;
pub const ECONNREFUSED: u32 = 111;
pub const EHOSTDOWN: u32 = 112;
pub const EHOSTUNREACH: u32 = 113;
pub const EALREADY: u32 = 114;
pub const EINPROGRESS: u32 = 115;
pub const ESTALE: u32 = 116;
pub const EUCLEAN: u32 = 117;
pub const ENOTNAM: u32 = 118;
pub const ENAVAIL: u32 = 119;
pub const EISNAM: u32 = 120;
pub const EREMOTEIO: u32 = 121;
pub const EDQUOT: u32 = 122;
pub const ENOMEDIUM: u32 = 123;
pub const EMEDIUMTYPE: u32 = 124;
pub const ECANCELED: u32 = 125;
pub const ENOKEY: u32 = 126;
pub const EKEYEXPIRED: u32 = 127;
pub const EKEYREVOKED: u32 = 128;
pub const EKEYREJECTED: u32 = 129;
pub const EOWNERDEAD: u32 = 130;
pub const ENOTRECOVERABLE: u32 = 131;
pub const ERFKILL: u32 = 132;
pub const EHWPOISON: u32 = 133;
pub const ENOTSUP: u32 = 95;
pub type wchar_t = ::std::os::raw::c_uint;
#[repr(C)]
#[repr(align(16))]
#[derive(Debug, Copy, Clone)]
pub struct max_align_t {
pub __clang_max_align_nonce1: ::std::os::raw::c_longlong,
pub __bindgen_padding_0: u64,
pub __clang_max_align_nonce2: u128,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of max_align_t"][::std::mem::size_of::<max_align_t>() - 32usize];
["Alignment of max_align_t"][::std::mem::align_of::<max_align_t>() - 16usize];
["Offset of field: max_align_t::__clang_max_align_nonce1"]
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce1) - 0usize];
["Offset of field: max_align_t::__clang_max_align_nonce2"]
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce2) - 16usize];
};
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct _GoString_ {
pub p: *const ::std::os::raw::c_char,
pub n: isize,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of _GoString_"][::std::mem::size_of::<_GoString_>() - 16usize];
["Alignment of _GoString_"][::std::mem::align_of::<_GoString_>() - 8usize];
["Offset of field: _GoString_::p"][::std::mem::offset_of!(_GoString_, p) - 0usize];
["Offset of field: _GoString_::n"][::std::mem::offset_of!(_GoString_, n) - 8usize];
};
unsafe extern "C" {
pub fn __errno_location() -> *mut ::std::os::raw::c_int;
}
pub type GoInt8 = ::std::os::raw::c_schar;
pub type GoUint8 = ::std::os::raw::c_uchar;
pub type GoInt16 = ::std::os::raw::c_short;
pub type GoUint16 = ::std::os::raw::c_ushort;
pub type GoInt32 = ::std::os::raw::c_int;
pub type GoUint32 = ::std::os::raw::c_uint;
pub type GoInt64 = ::std::os::raw::c_longlong;
pub type GoUint64 = ::std::os::raw::c_ulonglong;
pub type GoInt = GoInt64;
pub type GoUint = GoUint64;
pub type GoUintptr = usize;
pub type GoFloat32 = f32;
pub type GoFloat64 = f64;
pub type GoComplex64 = __BindgenComplex<f32>;
pub type GoComplex128 = __BindgenComplex<f64>;
pub type _check_for_64_bit_pointer_matching_GoInt = [::std::os::raw::c_char; 1usize];
pub type GoString = _GoString_;
pub type GoMap = *mut ::std::os::raw::c_void;
pub type GoChan = *mut ::std::os::raw::c_void;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GoInterface {
pub t: *mut ::std::os::raw::c_void,
pub v: *mut ::std::os::raw::c_void,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of GoInterface"][::std::mem::size_of::<GoInterface>() - 16usize];
["Alignment of GoInterface"][::std::mem::align_of::<GoInterface>() - 8usize];
["Offset of field: GoInterface::t"][::std::mem::offset_of!(GoInterface, t) - 0usize];
["Offset of field: GoInterface::v"][::std::mem::offset_of!(GoInterface, v) - 8usize];
};
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GoSlice {
pub data: *mut ::std::os::raw::c_void,
pub len: GoInt,
pub cap: GoInt,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of GoSlice"][::std::mem::size_of::<GoSlice>() - 24usize];
["Alignment of GoSlice"][::std::mem::align_of::<GoSlice>() - 8usize];
["Offset of field: GoSlice::data"][::std::mem::offset_of!(GoSlice, data) - 0usize];
["Offset of field: GoSlice::len"][::std::mem::offset_of!(GoSlice, len) - 8usize];
["Offset of field: GoSlice::cap"][::std::mem::offset_of!(GoSlice, cap) - 16usize];
};
unsafe extern "C" {
pub fn TsnetNewServer() -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetStart(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetUp(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetClose(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetGetIps(
sd: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetErrmsg(
sd: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetListen(
sd: ::std::os::raw::c_int,
network: *mut ::std::os::raw::c_char,
addr: *mut ::std::os::raw::c_char,
listenerOut: *mut ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetGetRemoteAddr(
listener: ::std::os::raw::c_int,
conn: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetDial(
sd: ::std::os::raw::c_int,
network: *mut ::std::os::raw::c_char,
addr: *mut ::std::os::raw::c_char,
connOut: *mut ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetDir(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetHostname(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetAuthKey(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetControlURL(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetEphemeral(sd: ::std::os::raw::c_int, e: GoInt) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetLogFD(
sd: ::std::os::raw::c_int,
fd: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetLoopback(
sd: ::std::os::raw::c_int,
addrOut: *mut ::std::os::raw::c_char,
addrLen: usize,
proxyOut: *mut ::std::os::raw::c_char,
localOut: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetEnableFunnelToLocalhostPlaintextHttp1(
sd: ::std::os::raw::c_int,
localhostPort: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}

View File

@ -1,350 +0,0 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use std::ffi::{CStr, CString};
use std::io::{Read, Write};
#[cfg(not(target_os = "windows"))]
use std::os::fd::{AsRawFd, RawFd};
#[cfg(target_os = "windows")]
use std::os::windows::io::{AsRawHandle, RawHandle};
use std::os::raw::{c_char, c_int};
mod bindings;
#[cfg(test)]
mod test;
type GoInt = i64;
use bindings::*;
use libc;
#[derive(Debug)]
pub enum TailscaleError {
ApiError(c_int, String),
BadFileDescriptor,
BufferTooSmall,
NulError(std::ffi::NulError),
InvalidUtf8(std::str::Utf8Error),
IoError(std::io::Error),
InvalidHandle,
}
impl From<std::ffi::NulError> for TailscaleError {
fn from(err: std::ffi::NulError) -> Self {
TailscaleError::NulError(err)
}
}
impl From<std::str::Utf8Error> for TailscaleError {
fn from(err: std::str::Utf8Error) -> Self {
TailscaleError::InvalidUtf8(err)
}
}
impl From<std::io::Error> for TailscaleError {
fn from(err: std::io::Error) -> Self {
TailscaleError::IoError(err)
}
}
// Helper function to get error message from the server handle
// This helper is needed because TsnetErrmsg requires the server handle (sd)
fn get_tsnet_errmsg(sd: c_int) -> String {
let mut buf = [0u8; 256]; // Choose a reasonable buffer size
let message = unsafe { TsnetErrmsg(sd, buf.as_mut_ptr() as *mut c_char, buf.len()) };
if message == 0 {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_string_lossy().into_owned()
} else {
format!(
"(Failed to get error message, TsnetErrmsg returned {})",
message
)
}
}
fn parse_tsnet_result(sd: c_int, ret: c_int) -> Result<(), TailscaleError> {
match ret {
0 => Ok(()),
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
_ => {
let message = get_tsnet_errmsg(sd);
Err(TailscaleError::ApiError(ret, message))
}
}
}
pub struct Tailscale(c_int);
// A TailscaleListener is a socket on the tailnet listening for connections.
//
// It is much like allocating a system socket(2) and calling listen(2).
// Accept connections with tailscale_accept and close the listener with close.
//
// Under the hood, a tailscale_listener is one half of a socketpair itself,
// used to move the connection fd from Go to C. This means you can use epoll
// or its equivalent on a tailscale_listener to know if there is a connection
// read to accept.
pub struct TailscaleListener(c_int);
// A TailscaleConn is a connection to an address on the tailnet.
//
// It is a pipe(2) on which you can use read(2), write(2), and close(2).
// For extra control over the connection, see the tailscale_conn_* functions.
pub struct TailscaleConn(c_int);
// NEEDS REVIEW. CANNOT BE BADLY DONE
impl Drop for Tailscale {
fn drop(&mut self) {
let ret = unsafe { TsnetClose(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale server {}: {}", self.0, ret);
}
}
}
impl Drop for TailscaleListener {
fn drop(&mut self) {
// TailscaleListener is treated like a file descriptor.
let ret = unsafe { libc::close(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale listener {}: {}", self.0, ret);
}
}
}
impl Drop for TailscaleConn {
fn drop(&mut self) {
// TailscaleConn is treated like a file descriptor (pipe).
let ret = unsafe { libc::close(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale connection {}: {}", self.0, ret);
}
}
}
impl Tailscale {
pub fn new() -> Self {
Tailscale(unsafe { TsnetNewServer() })
}
pub fn start(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetStart(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn up(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetUp(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn close(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetClose(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn set_dir(&self, dir: &str) -> Result<(), TailscaleError> {
let c_dir = CString::new(dir)?;
let ret = unsafe { TsnetSetDir(self.0, c_dir.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_hostname<T: AsRef<str>>(&self, hostname: T) -> Result<(), TailscaleError> {
let c_hostname = CString::new(hostname.as_ref())?;
let ret = unsafe { TsnetSetHostname(self.0, c_hostname.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_authkey(&self, authkey: &str) -> Result<(), TailscaleError> {
let c_authkey = CString::new(authkey)?;
let ret = unsafe { TsnetSetAuthKey(self.0, c_authkey.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_control_url(&self, control_url: &str) -> Result<(), TailscaleError> {
let c_control_url = CString::new(control_url)?;
let ret = unsafe { TsnetSetControlURL(self.0, c_control_url.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_ephemeral(&self, ephemeral: bool) -> Result<(), TailscaleError> {
let e: GoInt = if ephemeral { 1 } else { 0 };
// Use GoInt (i64) based on bindgen output
let ret = unsafe { TsnetSetEphemeral(self.0, e) };
parse_tsnet_result(self.0, ret)
}
pub fn set_log_fd(&self, fd: i32) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetSetLogFD(self.0, fd) };
parse_tsnet_result(self.0, ret)
}
pub fn get_ips<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
let ret = unsafe { TsnetGetIps(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
match ret {
0 => {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
}
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
_ => {
let err_msg = get_tsnet_errmsg(self.0);
Err(TailscaleError::ApiError(ret, err_msg))
}
}
}
pub fn loopback(
&self,
addr_buf: &mut [u8],
proxy_buf: &mut [u8],
local_buf: &mut [u8],
) -> Result<(), TailscaleError> {
// C header says proxy_out and local_out must hold 33 bytes.
if proxy_buf.len() < 33 || local_buf.len() < 33 {
return Err(TailscaleError::BufferTooSmall); // Custom check based on docs
}
let ret = unsafe {
TsnetLoopback(
self.0,
addr_buf.as_mut_ptr() as *mut c_char,
addr_buf.len(),
proxy_buf.as_mut_ptr() as *mut c_char,
local_buf.as_mut_ptr() as *mut c_char,
)
};
parse_tsnet_result(self.0, ret)
}
pub fn dial(&self, network: &str, addr: &str) -> Result<TailscaleConn, TailscaleError> {
let c_network = CString::new(network)?;
let c_addr = CString::new(addr)?;
let mut conn_out: c_int = -1;
let ret = unsafe {
TsnetDial(
self.0,
c_network.as_ptr() as *mut c_char,
c_addr.as_ptr() as *mut c_char,
&mut conn_out,
)
};
parse_tsnet_result(self.0, ret)?;
if ret == 0 && conn_out != -1 {
Ok(TailscaleConn(conn_out))
} else if ret == 0 {
Err(TailscaleError::InvalidHandle)
} else {
unreachable!();
}
}
pub fn listen(&self, network: &str, addr: &str) -> Result<TailscaleListener, TailscaleError> {
let c_network = CString::new(network)?;
let c_addr = CString::new(addr)?;
let mut listener_out: c_int = -1; // Use c_int for the output pointer
let ret = unsafe {
TsnetListen(
self.0,
c_network.as_ptr() as *mut c_char,
c_addr.as_ptr() as *mut c_char,
&mut listener_out,
)
};
parse_tsnet_result(self.0, ret)?;
if ret == 0 && listener_out != -1 {
Ok(TailscaleListener(listener_out))
} else if ret == 0 {
Err(TailscaleError::InvalidHandle)
} else {
unreachable!();
}
}
pub fn enable_funnel_to_localhost_plaintext_http1(
&self,
localhost_port: i32,
) -> Result<(), TailscaleError> {
let ret =
unsafe { TsnetEnableFunnelToLocalhostPlaintextHttp1(self.0, localhost_port as c_int) };
parse_tsnet_result(self.0, ret)
}
pub fn get_last_error_message<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
let ret = unsafe { TsnetErrmsg(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
match ret {
0 => {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
}
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
// TsnetErrmsg should ideally not return other codes, but handle defensively
_ => Err(TailscaleError::ApiError(
ret,
format!("TsnetErrmsg returned unknown code {}", ret),
)),
}
}
}
#[cfg(not(target_os = "windows"))]
// Requires the connection handle to behave like a raw file descriptor.
impl AsRawFd for TailscaleConn {
fn as_raw_fd(&self) -> RawFd {
self.0
}
}
#[cfg(target_os = "windows")]
impl AsRawHandle for TailscaleConn {
fn as_raw_handle(&self) -> RawHandle {
self.0
}
}
impl Read for TailscaleConn {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
#[cfg(not(target_os = "windows"))]
let fd = self.as_raw_fd();
#[cfg(target_os = "windows")]
let fd = self.as_raw_handle();
// Safety: Calling libc::read on a valid file descriptor.
// The caller must ensure the handle is valid for reading (it is after successful dial/accept).
let n = unsafe {
libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
}
impl Write for TailscaleConn {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let fd = self.as_raw_fd();
// Safety: Calling libc::write on a valid file descriptor.
// The caller must ensure the handle is valid for writing (it is after successful dial/accept).
let n = unsafe {
libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
fn flush(&mut self) -> std::io::Result<()> {
// For a pipe/socket, flush is often a no-op after write.
Ok(())
}
}

View File

@ -1,30 +0,0 @@
use std::error::Error;
use crate::{Tailscale, TailscaleError};
#[test]
fn start_listener() -> Result<(), TailscaleError> {
println!("Creating server");
// Create a new server
let ts = Tailscale::new();
// Configure it
println!("Configuring directory");
ts.set_dir("/tmp/tailscale-rust-test")?;
println!("Configuring hostname");
ts.set_hostname("my-rust-node")?;
println!("Setting ephemeral");
//ts.set_authkey("tskey-...")?; // Set authkey if needed for auto-registration
ts.set_ephemeral(true)?;
// Bring the server up
println!("Starting Tailscale...");
ts.up()?;
println!("Tailscale started!");
// Get IPs
let mut ip_buf = [0u8; 256];
let ips = ts.get_ips(&mut ip_buf)?;
println!("Tailscale IPs: {}", ips);
Ok(())
}

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.3.0-rc-2", "version": "0.2.0-beta",
"identifier": "dev.drop.app", "identifier": "dev.drop.app",
"build": { "build": {
"beforeDevCommand": "yarn dev --port 1432", "beforeDevCommand": "yarn dev --port 1432",

View File

@ -17,7 +17,7 @@ export type User = {
username: string; username: string;
admin: boolean; admin: boolean;
displayName: string; displayName: string;
profilePictureObjectId: string; profilePicture: string;
}; };
export type AppState = { export type AppState = {
@ -30,20 +30,14 @@ export type Game = {
mName: string; mName: string;
mShortDescription: string; mShortDescription: string;
mDescription: string; mDescription: string;
mIconObjectId: string; mIconId: string;
mBannerObjectId: string; mBannerId: string;
mCoverObjectId: string; mCoverId: string;
mImageLibraryObjectIds: string[]; mImageLibrary: string[];
mImageCarouselObjectIds: 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; }
};

Some files were not shown because too many files have changed in this diff Show More