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
213 changed files with 9901 additions and 34167 deletions

View File

@ -1,23 +0,0 @@
on: push
name: Clippy check
jobs:
clippy_check:
runs-on: ubuntu-24.04
permissions:
checks: write
steps:
- uses: actions/checkout@v1
- name: install dependencies (ubuntu only)
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path ./src-tauri/Cargo.toml

View File

@ -1,104 +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-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-22.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' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-latest'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
curl https://droposs.org/drop.crt --output drop.pem
sudo security authorizationdb write com.apple.trust-settings.user allow
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.user
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
if: matrix.platform == 'macos-latest'
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported. Using identity: $CERT_ID"
- 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 }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
NO_STRIP: true
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 }}

5
.gitignore vendored
View File

@ -26,7 +26,4 @@ dist-ssr
.output
src-tauri/flamegraph.svg
src-tauri/perf*
/*.AppImage
/squashfs-root
src-tauri/perf*

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "src-tauri/tailscale/libtailscale"]
path = src-tauri/tailscale/libtailscale
url = https://github.com/tailscale/libtailscale.git
[submodule "libs/drop-base"]
path = libs/drop-base
url = https://github.com/drop-oss/drop-base.git

View File

@ -1,21 +1,29 @@
# Drop Desktop Client
# Drop App
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
## Internals
## Running
Before setting up the drop app, be sure that you have a server set up.
The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
## Current features
Currently supported are the following features:
- Signin (with custom server)
- Database registering & recovery
- Dynamic library fetching from server
- Installing & uninstalling games
- Download progress monitoring
- Launching / playing games
## Development
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
Install dependencies with `yarn`
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
e.g. `RUST_LOG=debug yarn tauri dev`
## Contributing
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.

View File

@ -1,5 +1,4 @@
<template>
<NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
@ -10,6 +9,8 @@
import "~/composables/downloads.js";
import { invoke } from "@tauri-apps/api/core";
import { AppStatus } from "~/types";
import { listen } from "@tauri-apps/api/event";
import { useAppState } from "./composables/app-state.js";
import {
initialNavigation,
@ -19,26 +20,18 @@ import {
const router = useRouter();
const state = useAppState();
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
async function fetchState() {
router.beforeEach(async () => {
try {
state.value = JSON.parse(await invoke("fetch_state"));
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: `App state is: ${state.value}`,
fatal: true,
});
} catch (e) {
console.error("failed to parse state", e);
throw e;
}
}
await fetchState();
// This is inefficient but apparently we do it lol
router.beforeEach(async () => {
await fetchState();
});
setupHooks();

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -1,55 +0,0 @@
import fs from "fs";
import process from "process";
import childProcess from "child_process";
import createLogger from "pino";
const OUTPUT = "./.output";
const logger = createLogger({ transport: { target: "pino-pretty" } });
async function spawn(exec, opts) {
const output = childProcess.spawn(exec, { ...opts, shell: true });
output.stdout.on("data", (data) => {
process.stdout.write(data);
});
output.stderr.on("data", (data) => {
process.stderr.write(data);
});
return await new Promise((resolve, reject) => {
output.on("error", (err) => reject(err));
output.on("exit", () => resolve());
});
}
const expectedLibs = ["drop-base/package.json"];
for (const lib of expectedLibs) {
const path = `./libs/${lib}`;
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
}
const views = fs.readdirSync(".").filter((view) => {
const expectedPath = `./${view}/package.json`;
return fs.existsSync(expectedPath);
});
fs.mkdirSync(OUTPUT, { recursive: true });
for (const view of views) {
const loggerChild = logger.child({});
process.chdir(`./${view}`);
loggerChild.info(`Install deps for "${view}"`);
await spawn("yarn");
loggerChild.info(`Building "${view}"`);
await spawn("yarn build", {
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
});
process.chdir("..");
fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, {
recursive: true,
});
}

View File

@ -0,0 +1,108 @@
<template>
<div class="inline-flex divide-x divide-zinc-900">
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]">
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
{{ buttonNames[props.status.type] }}
</button>
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
<div class="h-full">
<MenuButton :class="[
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'
]">
<ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-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
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">
<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>
</div>
</MenuItems>
</transition>
</Menu>
</div>
</template>
<script setup lang="ts">
import {
ArrowDownTrayIcon,
ChevronDownIcon,
PlayIcon,
QueueListIcon,
TrashIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
import type { Component } from "vue";
import { GameStatusEnum, type GameStatus } from "~/types.js";
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
const props = defineProps<{ status: GameStatus }>();
const emit = defineEmits<{
(e: "install"): void;
(e: "launch"): void;
(e: "queue"): void;
(e: "uninstall"): void;
(e: "kill"): void;
}>();
const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired);
const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
[GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
[GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
[GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Running]: "bg-zinc-800 text-white focus-visible:outline-zinc-700"
};
const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Install",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading",
[GameStatusEnum.SetupRequired]: "Setup",
[GameStatusEnum.Installed]: "Play",
[GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop"
};
const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
[GameStatusEnum.Queued]: QueueListIcon,
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
[GameStatusEnum.SetupRequired]: WrenchIcon,
[GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon
};
const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.Remote]: () => emit("install"),
[GameStatusEnum.Queued]: () => emit("queue"),
[GameStatusEnum.Downloading]: () => emit("queue"),
[GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => { },
[GameStatusEnum.Running]: () => emit("kill")
};
</script>

View File

@ -2,14 +2,16 @@
<div class="h-16 bg-zinc-950 flex flex-row justify-between">
<div class="flex flex-row grow items-center pl-5 pr-2 py-3">
<div class="inline-flex items-center gap-x-10">
<Wordmark class="h-8 mb-0.5" />
<NuxtLink to="/store">
<Wordmark class="h-8 mb-0.5" />
</NuxtLink>
<nav class="inline-flex items-center mt-0.5">
<ol class="inline-flex items-center gap-x-6">
<NuxtLink
v-for="(nav, navIdx) in navigation"
:class="[
'transition uppercase font-display font-semibold text-md',
navIdx === currentNavigation
navIdx === currentPageIndex
? 'text-zinc-100'
: 'text-zinc-400 hover:text-zinc-200',
]"
@ -26,7 +28,9 @@
/>
<div class="inline-flex items-center">
<ol class="inline-flex gap-3">
<HeaderQueueWidget :object="currentQueueObject" />
<HeaderQueueWidget
:object="currentQueueObject"
/>
<li v-for="(item, itemIdx) in quickActions">
<HeaderWidget
@click="item.action"
@ -35,23 +39,21 @@
<component class="h-5" :is="item.icon" />
</HeaderWidget>
</li>
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
<HeaderUserWidget />
</ol>
</div>
</div>
<WindowControl />
<WindowControl class="h-16 w-16 p-4" />
</div>
</template>
<script setup lang="ts">
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 { getCurrentWindow } from "@tauri-apps/api/window";
const window = getCurrentWindow();
const state = useAppState();
const navigation: Array<NavigationItem> = [
{
@ -76,7 +78,7 @@ const navigation: Array<NavigationItem> = [
},
];
const { currentNavigation } = useCurrentNavigationIndex(navigation);
const currentPageIndex = useCurrentNavigationIndex(navigation);
const quickActions: Array<QuickActionNav> = [
{

View File

@ -0,0 +1,5 @@
<template>
<button class="transition h-10 w-10 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-2">
<slot />
</button>
</template>

View File

@ -1,5 +1,5 @@
<template>
<Menu v-if="state?.user" as="div" class="relative inline-block">
<Menu v-if="state.user" as="div" class="relative inline-block">
<MenuButton>
<HeaderWidget>
<div class="inline-flex items-center text-zinc-300 hover:text-white">
@ -23,7 +23,7 @@
<MenuItems
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
>
<div class="flex-col gap-y-2">
<PanelWidget class="flex-col gap-y-2">
<NuxtLink
to="/id/me"
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
@ -37,7 +37,7 @@
</NuxtLink>
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
<div class="flex flex-col mb-1">
<MenuItem v-if="state.user.admin" v-slot="{ active }">
<MenuItem v-slot="{ active }">
<a
:href="adminUrl"
target="_blank"
@ -49,23 +49,20 @@
Admin Dashboard
</a>
</MenuItem>
<MenuItem
v-for="(nav, navIdx) in navigation"
v-slot="{ active, close }"
>
<MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active, close }">
<button
@click="() => navigate(close, nav)"
@click="() => navigate(close, nav)"
:href="nav.route"
:class="[
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
'transition text-left block px-4 py-2 text-sm',
]"
>
{{ nav.label }}
</button>
{{ nav.label }}</button
>
</MenuItem>
</div>
</div>
</PanelWidget>
</MenuItems>
</transition>
</Menu>
@ -76,28 +73,34 @@ import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
import type { NavigationItem } from "../types";
import HeaderWidget from "./HeaderWidget.vue";
import { useAppState } from "~/composables/app-state";
import { invoke } from "@tauri-apps/api/core";
const open = ref(false);
const router = useRouter();
router.afterEach(() => {
open.value = false;
});
})
const state = useAppState();
const profilePictureUrl: string = await useObject(
state.value?.user?.profilePictureObjectId ?? ""
);
const profilePictureUrl: string = await invoke("gen_drop_url", {
path: `/api/v1/object/${state.value.user?.profilePicture}`,
});
const adminUrl: string = await invoke("gen_drop_url", {
path: "/admin",
});
function navigate(close: () => any, to: NavigationItem) {
function navigate(close: () => any, to: NavigationItem){
close();
router.push(to.route);
}
const navigation: NavigationItem[] = [
{
label: "Account settings",
route: "/account",
prefix: "",
},
{
label: "App settings",
route: "/settings",
@ -107,6 +110,6 @@ const navigation: NavigationItem[] = [
label: "Quit Drop",
route: "/quit",
prefix: "",
},
];
}
]
</script>

View File

@ -13,7 +13,11 @@
<div class="max-w-lg">
<slot />
<div class="mt-10">
<div>
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="text-sm text-left font-semibold leading-7 text-blue-600"
>
<div v-if="loading" role="status">
<svg
aria-hidden="true"
@ -33,19 +37,10 @@
</svg>
<span class="sr-only">Loading...</span>
</div>
<span class="inline-flex gap-x-8 items-center" v-else>
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
>
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
</button>
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
Use a code &rarr;
</NuxtLink>
<span v-else>
Sign in with your browser <span aria-hidden="true">&rarr;</span>
</span>
</div>
</button>
<div class="mt-5" v-if="offerManual">
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
@ -126,13 +121,11 @@
<script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/16/solid";
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const loading = ref(false);
const error = ref<string | undefined>();
let offerManualTimeout: NodeJS.Timeout | undefined;
const offerManual = ref(false);
const manualToken = ref("");
const manualLoading = ref(false);
@ -142,16 +135,14 @@ async function auth() {
}
function authWrapper_wrapper() {
error.value = undefined;
loading.value = true;
auth().catch((e) => {
loading.value = false;
error.value = e;
if (offerManualTimeout) clearTimeout(offerManualTimeout);
});
offerManualTimeout = setTimeout(() => {
setTimeout(() => {
offerManual.value = true;
}, 2000);
}, 10000);
}
async function continueManual() {

View File

@ -1,6 +1,6 @@
<template>
<div
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
>
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
<Wordmark class="mt-1" />

View File

@ -0,0 +1,7 @@
<template>
<NuxtLink
class="inline-flex items-center gap-x-2 px-1 py-0.5 rounded bg-blue-900 text-zinc-100 hover:bg-blue-800"
>
<slot />
</NuxtLink>
</template>

View File

@ -1,7 +1,4 @@
<template>
<HeaderButton v-if="showMinimise" @click="() => minimise()">
<MinusIcon />
</HeaderButton>
<HeaderButton @click="() => close()">
<XMarkIcon />
</HeaderButton>
@ -11,14 +8,11 @@
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
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 showMinimise = await window.isMinimizable();
async function close() {
await window.close();
}
async function minimise() {
await window.minimize();
}
</script>

3
composables/app-state.ts Normal file
View File

@ -0,0 +1,3 @@
import type { AppState } from "~/types";
export const useAppState = () => useState<AppState>("state");

View File

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

View File

@ -32,5 +32,3 @@ listen("update_stats", (event) => {
const stats = useStatsState();
stats.value = event.payload as StatsState;
});
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);

View File

@ -1,9 +1,8 @@
import { invoke } from "@tauri-apps/api/core";
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> } = {};
@ -14,6 +13,7 @@ export type SerializedGameStatus = [
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) {
return {
type: status[0].type,
@ -31,44 +31,27 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
export const useGame = async (gameId: string) => {
if (!gameRegistry[gameId]) {
const data: {
game: Game;
status: SerializedGameStatus;
version?: GameVersion;
} = await invoke("fetch_game", {
gameId,
});
gameRegistry[gameId] = { game: data.game, version: data.version };
const data: { game: Game; status: SerializedGameStatus } = await invoke(
"fetch_game",
{
gameId,
}
);
gameRegistry[gameId] = data.game;
if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => {
console.log(event);
const payload: {
status: SerializedGameStatus;
version?: GameVersion;
} = event.payload as any;
console.log(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 status = gameStatusRegistry[gameId];
return { ...game, status };
};
export type FrontendGameConfiguration = {
launchString: string;
};
return { game, status };
};

View File

@ -1,11 +1,9 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types";
export function setupHooks() {
const router = useRouter();
const state = useAppState();
listen("auth/processing", (event) => {
router.push("/auth/processing");
@ -17,9 +15,8 @@ export function setupHooks() {
);
});
listen("auth/finished", async (event) => {
router.push("/library");
state.value = JSON.parse(await invoke("fetch_state"));
listen("auth/finished", (event) => {
router.push("/store");
});
listen("download_error", (event) => {
@ -30,31 +27,12 @@ export function setupHooks() {
description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string
).toString()}"`,
buttonText: "Close",
buttonText: "Close"
},
(e, c) => c()
);
});
// This is for errors that (we think) aren't our fault
listen("launch_external_error", (event) => {
createModal(
ModalType.Confirmation,
{
title: "Did something go wrong?",
description:
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
buttonText: "Open",
},
async (e, c) => {
if (e == "confirm") {
await invoke("open_process_logs", { gameId: event.payload });
}
c();
}
);
});
/*
document.addEventListener("contextmenu", (event) => {
@ -65,13 +43,7 @@ export function setupHooks() {
*/
}
export function initialNavigation(state: ReturnType<typeof useAppState>) {
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: "App state not valid",
fatal: true,
});
export function initialNavigation(state: Ref<AppState>) {
const router = useRouter();
switch (state.value.status) {
@ -88,6 +60,6 @@ export function initialNavigation(state: ReturnType<typeof useAppState>) {
router.push("/error/serverunavailable");
break;
default:
router.push("/library");
router.push("/store");
}
}

View File

@ -1,5 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core";
export const useObject = (id: string) => {
export const useObject = async (id: string) => {
return convertFileSrc(id, "object");
};

View File

@ -1,6 +1,6 @@
<template>
<div tvnav-id="mainbody" class="flex flex-row bg-zinc-900 overflow-hidden h-screen">
<Sidebar class="select-none" />
<div class="flex flex-col bg-zinc-900 overflow-hidden">
<Header class="select-none" />
<div class="relative grow overflow-y-auto">
<slot />
</div>

View File

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

Submodule libs/drop-base deleted from 04125e89be

View File

@ -1,43 +0,0 @@
<template>
<NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
</NuxtLayout>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const router = useRouter();
const state = useAppState();
async function fetchState() {
try {
state.value = JSON.parse(await invoke("fetch_state"));
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: `App state is: ${state.value}`,
fatal: true,
});
} catch (e) {
console.error("failed to parse state", e);
throw e;
}
}
await fetchState();
// This is inefficient but apparently we do it lol
router.beforeEach(async () => {
await fetchState();
});
setupHooks();
initialNavigation(state);
useHead({
title: "Drop",
});
</script>

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,5 +0,0 @@
<template>
<button class="transition h-full aspect-square text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-[1.1rem]">
<slot />
</button>
</template>

View File

@ -1,303 +0,0 @@
<template>
<div class="flex flex-col h-full">
<div class="mb-3 inline-flex gap-x-2">
<div
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
>
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<MagnifyingGlassIcon
class="h-5 w-5 text-zinc-400"
aria-hidden="true"
/>
</div>
<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>
<button
@click="() => calculateGames(true, true)"
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
>
<ArrowPathIcon class="size-4" />
</button>
</div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<Disclosure
as="div"
v-for="(nav, navIndex) in filteredNavigation"
:key="nav.id"
class="first:pt-0 last:pb-0"
v-slot="{ open }"
:default-open="nav.deft"
>
<dt>
<DisclosureButton
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
>
<span class="text-sm font-semibold font-display">{{
nav.name
}}</span>
<span class="ml-6 flex h-7 items-center">
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
</span>
</DisclosureButton>
</dt>
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
<NuxtLink
v-for="item in nav.items"
:key="nav.id"
:class="[
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
currentNavigation == item.id
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: item.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="item.route"
>
<div class="flex items-center w-full gap-x-2">
<div
class="flex-none transition-transform duration-300 hover:-rotate-2"
>
<img
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
:src="icons[item.id]"
alt=""
/>
</div>
<div class="flex flex-col gap-x-2">
<p
class="text-sm whitespace-nowrap font-display font-semibold"
>
{{ item.label }}
</p>
<p
class="truncate text-[10px] font-bold uppercase font-display"
:class="[
gameStatusTextStyle[games[item.id].status.value.type],
]"
>
{{ gameStatusText[games[item.id].status.value.type] }}
</p>
</div>
</div>
</NuxtLink>
</DisclosurePanel>
</Disclosure>
</TransitionGroup>
<div
v-if="loading"
class="h-full grow flex p-8 justify-center text-zinc-100"
>
<div role="status">
<svg
aria-hidden="true"
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
import {
ArrowPathIcon,
MagnifyingGlassIcon,
MinusSmallIcon,
PlusSmallIcon,
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import {
GameStatusEnum,
type Collection as Collection,
type Game,
type GameStatus,
} from "~/types";
import { 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-zinc-400",
[GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-700",
[GameStatusEnum.Queued]: "text-zinc-400",
[GameStatusEnum.Updating]: "text-zinc-400",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading...",
[GameStatusEnum.Validating]: "Validating...",
[GameStatusEnum.Installed]: "Installed",
[GameStatusEnum.Updating]: "Updating...",
[GameStatusEnum.Uninstalling]: "Uninstalling...",
[GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running",
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
};
const router = useRouter();
const searchQuery = ref("");
const loading = ref(false);
const games: {
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
} = {};
const icons: { [key: string]: string } = {};
const collections: Ref<Collection[]> = ref([]);
async function calculateGames(clearAll = false, forceRefresh = false) {
if (clearAll) {
collections.value = [];
loading.value = true;
}
// If we update immediately, the navigation gets re-rendered before we
// add all the necessary state, and it freaks tf out
const newGames = await invoke<Game[]>("fetch_library", {
hardRefresh: forceRefresh,
});
const otherCollections = await invoke<Collection[]>("fetch_collections", {
hardRefresh: forceRefresh,
});
const allGames = [
...newGames,
...otherCollections
.map((e) => e.entries)
.flat()
.map((e) => e.game),
].filter((v, i, a) => a.indexOf(v) === i);
for (const game of allGames) {
if (games[game.id]) continue;
games[game.id] = await useGame(game.id);
}
for (const game of allGames) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId);
}
const libraryCollection = {
id: "library",
name: "Library",
isDefault: true,
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
} satisfies Collection;
loading.value = false;
collections.value = [libraryCollection, ...otherCollections];
}
// Wait up to 300 ms for the library to load, otherwise
// show the loading state while we while
await new Promise<void>((r) => {
let hasResolved = false;
const resolveFunc = () => {
if (!hasResolved) r();
hasResolved = true;
};
calculateGames(true).then(resolveFunc);
setTimeout(resolveFunc, 300);
});
const navigation = computed(() =>
collections.value.map((collection) => {
const items = collection.entries.map(({ game }) => {
const status = games[game.id].status;
const isInstalled = computed(
() => status.value.type != GameStatusEnum.Remote
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
id: game.id,
};
return item;
});
return {
id: collection.id,
name: collection.name,
deft: collection.isDefault,
items,
};
})
);
const route = useRoute();
const currentNavigation = computed(() => {
return route.path.slice("/library/".length);
});
const filteredNavigation = computed(() => {
if (!searchQuery.value)
return navigation.value.map((e, i) => ({ ...e, index: i }));
const query = searchQuery.value.toLowerCase();
return navigation.value
.map((c) => ({
...c,
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
}))
.filter((e) => e.items.length > 0);
});
listen("update_library", async (event) => {
console.log("Updating library");
let oldNavigation = currentNavigation.value;
await calculateGames();
if (oldNavigation !== currentNavigation.value) {
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,91 +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,82 +0,0 @@
<template>
<div class="flex flex-col bg-zinc-900 overflow-hidden h-screen">
<NuxtErrorBoundary>
<Header class="select-none" />
<div class="relative grow overflow-y-auto">
<slot />
</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>
</template>
<script setup lang="ts">
const queueState = useQueueState();
</script>

View File

@ -1,25 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
ssr: false,
extends: ["../shared", "../libs/drop-base"],
app: {
baseURL: "/main",
},
devtools: {
enabled: false,
},
});

View File

@ -1,37 +0,0 @@
{
"name": "view",
"private": true,
"version": "0.3.3",
"type": "module",
"scripts": {
"build": "nuxt generate",
"dev": "nuxt dev",
"postinstall": "nuxt prepare",
"tauri": "tauri"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": "^2.7.0",
"koa": "^2.16.1",
"markdown-it": "^14.1.0",
"micromark": "^4.0.1",
"nuxt": "^3.16.0",
"scss": "^0.2.4",
"vue-router": "latest",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -1,37 +0,0 @@
<template>
<div class="min-h-full w-full flex items-center justify-center">
<div class="flex flex-col items-center">
<div class="text-center">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Device authorization
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-md mx-auto">
Open Drop on another one of your devices, and use your account
dropdown to "Authorize client", and enter the code below.
</p>
<div
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
>
<span v-for="letter in code.split('')">{{ letter }}</span>
</div>
</div>
<div class="mt-10 flex items-center justify-center gap-x-6">
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
><span aria-hidden="true">&larr;</span> Use a different method
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const code = await invoke<string>("auth_initiate_code");
definePageMeta({
layout: "mini",
});
</script>

View File

@ -1,25 +0,0 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
WrenchScrewdriverIcon,
} from "@heroicons/vue/20/solid";
</script>

View File

@ -1,52 +0,0 @@
<template>
<div class="flex flex-row h-full">
<!-- Sidebar -->
<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"
>
<LibrarySearch />
</div>
<div class="grow overflow-y-auto">
<NuxtErrorBoundary>
<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>
</template>
<script setup lang="ts"></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,674 +0,0 @@
<template>
<div
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
>
<div class="absolute inset-0 z-0">
<img
:src="bannerUrl"
class="w-full h-[24rem] object-cover blur-sm scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-zinc-900/80 to-transparent opacity-90"
/>
<div
class="absolute inset-0 bg-gradient-to-r from-zinc-900/95 via-zinc-900/80 to-transparent opacity-90"
/>
</div>
<div class="relative z-10">
<div class="px-8 pb-4">
<h1
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
>
{{ game.mName }}
</h1>
<div class="flex flex-row gap-x-4 items-stretch mb-8">
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<GameStatusButton
@install="() => installFlow()"
@launch="() => launch()"
@queue="() => queue()"
@uninstall="() => uninstall()"
@kill="() => kill()"
@options="() => (configureModalOpen = true)"
@resume="() => resumeDownload()"
: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="index"
: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>
<ModalTemplate v-model="installFlowOpen">
<template #default>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100">
Install {{ game.mName }}?
</h3>
<div class="mt-2">
<p class="text-sm text-zinc-400">
Drop will add {{ game.mName }} to the queue to be downloaded.
While downloading, Drop may use up a large amount of resources,
particularly network bandwidth and CPU utilisation.
</p>
</div>
</div>
</div>
<form class="space-y-6">
<div v-if="versionOptions && versionOptions.length > 0">
<Listbox as="div" v-model="installVersionIndex">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Version</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate"
>{{ versionOptions[installVersionIndex].versionName }}
on
{{ versionOptions[installVersionIndex].platform }}</span
>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(version, versionIdx) in versionOptions"
:key="version.versionName"
:value="versionIdx"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected
? 'font-semibold text-zinc-100'
: 'font-normal',
'block truncate',
]"
>{{ version.versionName }} on
{{ version.platform }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div
v-else-if="versionOptions === null || versionOptions?.length == 0"
class="mt-1 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">
There are no supported versions to install. Please contact your
server admin or try again later.
</h3>
</div>
</div>
</div>
<div v-else class="w-full flex items-center justify-center p-4">
<div role="status">
<svg
aria-hidden="true"
class="w-7 h-7 text-transparent animate-spin fill-white"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-if="installDirs">
<Listbox as="div" v-model="installDir">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Install to</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate">{{
installDirs[installDir]
}}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(dir, dirIdx) in installDirs"
:key="dir"
:value="dirIdx"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected
? 'font-semibold text-zinc-100'
: 'font-normal',
'block truncate',
]"
>{{ dir }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
<div class="text-zinc-400 text-sm mt-2">
Add more install directories in
<PageWidget to="/settings/downloads">
<WrenchIcon class="size-3" />
Settings
</PageWidget>
</div>
</Listbox>
</div>
</form>
<div v-if="installError" class="mt-1 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">
{{ installError }}
</h3>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
@click="() => install()"
:disabled="!(versionOptions && versionOptions.length > 0)"
:loading="installLoading"
type="submit"
class="ml-2 w-full sm:w-fit"
>
Install
</LoadingButton>
<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-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="installFlowOpen = false"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
<!--
Dear future DecDuck,
This v-if is necessary for Vue rendering reasons
(it tries to access the game version for not installed games)
You have already tried to remove it
Don't.
-->
<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="index"
: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>
<script setup lang="ts">
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from "@headlessui/vue";
import {
CheckIcon,
ChevronUpDownIcon,
WrenchIcon,
ChevronLeftIcon,
ChevronRightIcon,
XMarkIcon,
ArrowsPointingOutIcon,
PhotoIcon,
} from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
import { micromark } from "micromark";
import { GameStatusEnum } from "~/types";
const route = useRoute();
const router = useRouter();
const id = route.params.id.toString();
const { game: rawGame, status } = await useGame(id);
const game = ref(rawGame);
const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`,
});
const bannerUrl = await useObject(game.value.mBannerObjectId);
// Get all available images
const mediaUrls = await Promise.all(
game.value.mImageCarouselObjectIds.map(async (v) => {
const src = await useObject(v);
return src;
})
);
const htmlDescription = micromark(game.value.mDescription);
const installFlowOpen = ref(false);
const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }>
>();
const installDirs = ref<undefined | Array<string>>();
const currentImageIndex = ref(0);
const configureModalOpen = ref(false);
async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
installDirs.value = undefined;
try {
versionOptions.value = await invoke("fetch_game_version_options", {
gameId: game.value.id,
});
installDirs.value = await invoke("fetch_download_dir_stats");
} catch (error) {
installError.value = (error as string).toString();
versionOptions.value = undefined;
}
}
const installLoading = ref(false);
const installError = ref<string | undefined>();
const installVersionIndex = ref(0);
const installDir = ref(0);
async function install() {
try {
if (!versionOptions.value) throw new Error("Versions have not been loaded");
installLoading.value = true;
await invoke("download_game", {
gameId: game.value.id,
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
installDir: installDir.value,
});
installFlowOpen.value = false;
} catch (error) {
installError.value = (error as string).toString();
}
installLoading.value = false;
}
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id });
} catch (e) {
console.error(e);
}
}
async function launch() {
try {
await invoke("launch_game", { id: game.value.id });
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't run "${game.value.mName}"`,
description: `Drop failed to launch "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
);
console.error(e);
}
}
async function queue() {
router.push("/queue");
}
async function uninstall() {
await invoke("uninstall_game", { gameId: game.value.id });
}
async function kill() {
try {
await invoke("kill_game", { gameId: game.value.id });
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't stop "${game.value.mName}"`,
description: `Drop failed to stop "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
);
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>
<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 +0,0 @@
<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">
import { RocketLaunchIcon } from '@heroicons/vue/24/outline';
</script>

View File

@ -1,25 +0,0 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
WrenchScrewdriverIcon,
} from "@heroicons/vue/20/solid";
</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,59 +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 space-y-8">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">
Start with system
</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Drop will automatically start when you log into your computer
</p>
</div>
<Switch
v-model="autostartEnabled"
:class="[
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="[
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>
</div>
</template>
<script setup lang="ts">
import { Switch } from "@headlessui/vue";
import { invoke } from "@tauri-apps/api/core";
defineProps<{}>();
const autostartEnabled = ref<boolean>(false);
// Load initial state
invoke("get_autostart_enabled").then((enabled) => {
autostartEnabled.value = enabled as boolean;
});
// Watch for changes and update autostart
watch(autostartEnabled, async (newValue: boolean) => {
try {
await invoke("toggle_autostart", { enabled: newValue });
} catch (error) {
console.error("Failed to toggle autostart:", error);
// Revert the toggle if it failed
autostartEnabled.value = !newValue;
}
});
</script>

View File

@ -1,23 +0,0 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<WrenchScrewdriverIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Under construction
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
This page hasn't been implemented yet.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { WrenchScrewdriverIcon } from "@heroicons/vue/20/solid";
</script>

View File

@ -1,37 +0,0 @@
<template>
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<BuildingStorefrontIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Store not supported in client
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
Currently, Drop requires you to view the store in your browser.
Please click the button below to open it in your default browser.
</p>
<NuxtLink
:href="storeUrl"
target="_blank"
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
BuildingStorefrontIcon,
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
</script>

View File

@ -1,21 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"../shared/components/**/*.vue"
],
theme: {
extend: {
fontFamily: {
sans: ["Inter"],
display: ["Motiva Sans"],
},
},
},
plugins: [require("@tailwindcss/forms"), require('@tailwindcss/typography')],
};

View File

@ -1,5 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"exclude": ["src-tauri/**/*"]
}

View File

@ -1,96 +0,0 @@
import type { Component } from "vue";
export type NavigationItem = {
prefix: string;
route: string;
label: string;
};
export type QuickActionNav = {
icon: Component;
notifications?: number;
action: () => Promise<void>;
};
export type User = {
id: string;
username: string;
admin: boolean;
displayName: string;
profilePictureObjectId: string;
};
export type AppState = {
status: AppStatus;
user?: User;
};
export type Game = {
id: string;
mName: string;
mShortDescription: string;
mDescription: string;
mIconObjectId: string;
mBannerObjectId: string;
mCoverObjectId: string;
mImageLibraryObjectIds: string[];
mImageCarouselObjectIds: string[];
};
export type Collection = {
id: string;
name: string;
isDefault: boolean;
entries: Array<{ gameId: string; game: Game }>;
};
export type GameVersion = {
launchCommandTemplate: string;
};
export enum AppStatus {
NotConfigured = "NotConfigured",
Offline = "Offline",
SignedOut = "SignedOut",
SignedIn = "SignedIn",
SignedInNeedsReauth = "SignedInNeedsReauth",
ServerUnavailable = "ServerUnavailable",
}
export enum GameStatusEnum {
Remote = "Remote",
Queued = "Queued",
Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed",
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
PartiallyInstalled = "PartiallyInstalled",
}
export type GameStatus = {
type: GameStatusEnum;
version_name?: string;
install_dir?: string;
};
export enum DownloadableType {
Game = "Game",
Tool = "Tool",
DLC = "DLC",
Mod = "Mod",
}
export type DownloadableMetadata = {
id: string;
version: string;
downloadType: DownloadableType;
};
export type Settings = {
autostart: boolean;
maxDownloadThreads: number;
forceOffline: boolean;
};

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,22 +0,0 @@
## This script is largely useless, because there's not much we can do about AppImage size
ARCH=$(uname -m)
# build tauri apps
# NO_STRIP=true yarn tauri build -- --verbose
# unpack appimage
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)
"$APPIMAGE" --appimage-extract
# strip binary
APPIMAGE_UNPACK="./squashfs-root"
find $APPIMAGE_UNPACK -type f -exec strip -s {} \;
APPIMAGETOOL=$(echo "obsolete-appimagetool-$ARCH.AppImage")
wget -O $APPIMAGETOOL "https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGETOOL"
chmod +x $APPIMAGETOOL
APPIMAGE_OUTPUT=$(./$APPIMAGETOOL $APPIMAGE_UNPACK | grep ".AppImage" | grep squashfs-root | awk '{ print $6 }')
mv $APPIMAGE_OUTPUT "$APPIMAGE"

View File

@ -1,22 +1,41 @@
{
"name": "drop-app",
"private": true,
"version": "0.2.0-beta",
"type": "module",
"scripts": {
"build": "node ./build.mjs",
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-deep-link": "^2.4.1",
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"pino": "^9.7.0",
"pino-pretty": "^13.1.1"
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-shell": ">=2.0.0",
"markdown-it": "^14.1.0",
"nuxt": "^3.13.0",
"scss": "^0.2.4",
"vue": "latest",
"vue-router": "latest",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.7.1"
}
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tauri-apps/cli": ">=2.0.0",
"@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13"
},
"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>

78
pages/library.vue Normal file
View File

@ -0,0 +1,78 @@
<template>
<div class="flex flex-row h-full">
<div
class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"
>
<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 class="grow overflow-y-auto">
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
let libraryDownloadError = false;
async function calculateGames(): Promise<Game[]> {
try {
return await invoke("fetch_library");
}
catch(e) {
libraryDownloadError = true;
return new Array();
}
}
const rawGames: Array<Game> = await calculateGames();
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
const icons = await Promise.all(
games.map(({ game, status }) => useObject(game.mIconId))
);
const navigation = games.map(({ game, 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,
};
return item;
});
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
</script>

View File

@ -0,0 +1,379 @@
<template>
<div
class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden"
>
<!-- banner image -->
<div class="absolute flex top-0 h-fit inset-x-0 z-[-20]">
<img :src="bannerUrl" class="w-full h-auto object-cover" />
<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
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
/>
</div>
<!-- main page -->
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<!-- game toolbar -->
<div class="h-full flex flex-row gap-x-4 items-stretch">
<GameStatusButton
@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"
>
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store
</a>
</div>
</div>
</div>
<ModalTemplate v-model="installFlowOpen">
<template #default>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
>Install {{ game.mName }}?
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-zinc-400">
Drop will add {{ game.mName }} to the queue to be downloaded.
While downloading, Drop may use up a large amount of resources,
particularly network bandwidth and CPU utilisation.
</p>
</div>
</div>
</div>
<form class="space-y-6">
<div v-if="versionOptions && versionOptions.length > 0">
<Listbox as="div" v-model="installVersionIndex">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Version</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate"
>{{ versionOptions[installVersionIndex].versionName }}
on
{{ versionOptions[installVersionIndex].platform }}</span
>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(version, versionIdx) in versionOptions"
:key="version.versionName"
:value="versionIdx"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected
? 'font-semibold text-zinc-100'
: 'font-normal',
'block truncate',
]"
>{{ version.versionName }} on
{{ version.platform }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div v-else class="mt-1 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">
There are no supported versions to install. Please contact your
server admin or try again later.
</h3>
</div>
</div>
</div>
<div v-if="installDirs">
<Listbox as="div" v-model="installDir">
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
>Install to</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
>
<span class="block truncate">{{
installDirs[installDir]
}}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(dir, dirIdx) in installDirs"
:key="dir"
:value="dirIdx"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected
? 'font-semibold text-zinc-100'
: 'font-normal',
'block truncate',
]"
>{{ dir }}</span
>
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
<div class="text-zinc-400 text-sm mt-2">
Add more install directories in
<PageWidget to="/settings/downloads">
<WrenchIcon class="size-3" />
Settings
</PageWidget>
</div>
</Listbox>
</div>
</form>
<div v-if="installError" class="mt-1 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">
{{ installError }}
</h3>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
@click="() => install()"
:disabled="
!(versionOptions && versionOptions.length > 0 && !installDir)
"
:loading="installLoading"
type="submit"
class="ml-2 w-full sm:w-fit"
>
Install
</LoadingButton>
<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-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="installFlowOpen = false"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>
<script setup lang="ts">
import {
Dialog,
DialogTitle,
TransitionChild,
TransitionRoot,
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from "@headlessui/vue";
import {
CheckIcon,
ChevronUpDownIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
const route = useRoute();
const router = useRouter();
const id = route.params.id.toString();
const { game: rawGame, status } = await useGame(id);
const game = ref(rawGame);
const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`,
});
const bannerUrl = await useObject(game.value.mBannerId);
const installFlowOpen = ref(false);
const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }>
>();
const installDirs = ref<undefined | Array<string>>();
async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
installDirs.value = undefined;
try {
versionOptions.value = await invoke("fetch_game_verion_options", {
gameId: game.value.id,
});
console.log(versionOptions.value);
installDirs.value = await invoke("fetch_download_dir_stats");
} catch (error) {
installError.value = (error as string).toString();
}
}
const installLoading = ref(false);
const installError = ref<string | undefined>();
const installVersionIndex = ref(0);
const installDir = ref(0);
async function install() {
try {
if (!versionOptions.value)
throw new Error("Versions have not been loaded");
installLoading.value = true;
await invoke("download_game", {
gameId: game.value.id,
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
installDir: installDir.value,
});
installFlowOpen.value = false;
} catch (error) {
installError.value = (error as string).toString();
}
installLoading.value = false;
}
async function launch() {
try {
await invoke("launch_game", { id: game.value.id });
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't run "${game.value.mName}"`,
description: `Drop failed to launch "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
);
console.error(e);
}
}
async function queue() {
router.push("/queue");
}
async function uninstall() {
await invoke("uninstall_game", { gameId: game.value.id });
}
async function kill() {
try {
await invoke("kill_game", { gameId: game.value.id });
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Couldn't stop "${game.value.mName}"`,
description: `Drop failed to stop "${game.value.mName}": ${e}`,
buttonText: "Close",
},
(e, c) => c()
);
console.error(e);
}
}
</script>

9
pages/library/index.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup lang="ts">
const props = defineProps<{ libraryDownloadError: boolean }>();
</script>
<template>
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
Library Failed to update
</div>
</template>

View File

@ -4,18 +4,18 @@
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
>
<div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
>
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
<span v-if="stats.time > 0" class="text-xs text-zinc-400"
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
<span v-if="stats.time > 0" class="text-sm"
>{{ formatTime(stats.time) }} left</span
>
</div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
<div
v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[3px] bg-blue-600 rounded-t-full"
class="w-[8px] bg-blue-600/40"
/>
</div>
</div>
@ -62,9 +62,9 @@
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}B</span>
}}</span>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
><ServerIcon class="size-5"
/></span>
</div>
@ -91,12 +91,7 @@
<script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => {
@ -105,12 +100,12 @@ window.addEventListener("resize", (event) => {
const queue = useQueueState();
const stats = useStatsState();
const speedHistory = useDownloadHistory();
const speedHistoryMax = computed(() => windowWidth.value / 4);
const speedHistory = useState<Array<number>>(() => []);
const speedHistoryMax = computed(() => windowWidth.value / 8);
const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
);
const previousGameId = useState<string | undefined>('previous_game');
const previousGameId = ref<string | undefined>();
const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
@ -122,15 +117,14 @@ function resetHistoryGraph() {
}
function checkReset(v: QueueState) {
const currentGame = v.queue.at(0)?.meta.id;
// If we don't have a game
if (!currentGame) return;
// If we're finished
if (!currentGame && previousGameId.value) {
previousGameId.value = undefined;
resetHistoryGraph();
return;
}
// If we don't have a game
if (!currentGame) return;
// If we started a new download
if (currentGame && !previousGameId.value) {
previousGameId.value = currentGame;
@ -150,10 +144,9 @@ watch(queue, (v) => {
});
watch(stats, (v) => {
if(v.speed == 0) return;
const newLength = speedHistory.value.push(v.speed);
if (newLength > speedHistoryMax.value) {
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
speedHistory.value.splice(0, 1);
}
checkReset(queue.value);
});
@ -165,7 +158,7 @@ function loadGamesForQueue(v: typeof queue.value) {
if (games.value[id]) return;
(async () => {
const gameData = await useGame(id);
const cover = await useObject(gameData.game.mCoverObjectId);
const cover = await useObject(gameData.game.mCoverId);
games.value[id] = { ...gameData, cover };
})();
}
@ -174,7 +167,7 @@ function loadGamesForQueue(v: typeof queue.value) {
loadGamesForQueue(queue.value);
async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_download_in_queue", {
await invoke("move_game_in_queue", {
oldIndex: event.oldIndex,
newIndex: event.newIndex,
});
@ -185,7 +178,7 @@ async function cancelGame(meta: DownloadableMetadata) {
}
function formatKilobytes(bytes: number): string {
const units = ["K", "M", "G", "T", "P"];
const units = ["KB", "MB", "GB", "TB", "PB"];
let value = bytes;
let unitIndex = 0;
const scalar = 1000;

View File

@ -10,13 +10,13 @@
<ul role="list" class="-mx-2 space-y-1">
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
<NuxtLink :href="item.route" :class="[
itemIdx === currentNavigation
itemIdx === currentPageIndex
? 'bg-zinc-800/50 text-zinc-100'
: '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',
]">
<component :is="item.icon" :class="[
itemIdx === currentNavigation
itemIdx === currentPageIndex
? 'text-zinc-100'
: 'text-zinc-400 group-hover:text-zinc-200',
'transition h-6 w-6 shrink-0',
@ -45,7 +45,6 @@ import type { Component } from "vue";
import type { NavigationItem } from "~/types";
import { platform } from '@tauri-apps/plugin-os';
import { invoke } from "@tauri-apps/api/core";
import { UserIcon } from "@heroicons/vue/20/solid";
const systemData = await invoke<{
clientId: string;
@ -102,12 +101,6 @@ const navigation = computed(() => [
prefix: "/settings/downloads",
icon: ArrowDownTrayIcon,
},
{
label: "Account",
route: "/settings/account",
prefix: "/settings/account",
icon: UserIcon
},
...(isDebugMode.value ? [{
label: "Debug Info",
route: "/settings/debug",
@ -119,10 +112,10 @@ const navigation = computed(() => [
const currentPlatform = platform();
// 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(navigation, (newNav) => {
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value;
currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
});
</script>

View File

@ -106,6 +106,8 @@ const systemData = await invoke<{
dataDir: string;
}>("fetch_system_data");
console.log(systemData);
clientId.value = systemData.clientId;
baseUrl.value = systemData.baseUrl;
dataDir.value = systemData.dataDir;

View File

@ -1,15 +1,10 @@
<template>
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
Downloads
</h3>
</div>
<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">
<div>
<div class="border-b border-zinc-600 py-2 px-1">
<div
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
>
<div class="ml-4 mt-2">
<h3 class="text-base font-display font-semibold text-zinc-100">
Install directories
</h3>
@ -20,17 +15,27 @@
</p>
</div>
<div class="ml-4 mt-2 shrink-0">
<button @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">
<button
@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
</button>
</div>
</div>
</div>
<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">
<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">
<p class="text-sm/6 text-zinc-100">
{{ dir }}
@ -38,12 +43,16 @@
</div>
</div>
<div class="flex shrink-0 items-center gap-x-6">
<button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[
dirs.length <= 1
? 'text-zinc-700'
: 'text-zinc-400 hover:text-zinc-100',
'-m-2.5 block p-2.5',
]">
<button
@click="() => deleteDirectory(dirIdx)"
:disabled="dirs.length <= 1"
:class="[
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>
<TrashIcon class="size-5" aria-hidden="true" />
</button>
@ -63,44 +72,37 @@
Maximum Download Threads
</label>
<div class="mt-2">
<input type="number" 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" />
<input
type="number"
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>
<p class="mt-2 text-sm text-zinc-400">
The maximum number of concurrent download threads. Higher values may
download faster but use more system resources. Default is 4.
</p>
</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">
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
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'
]">
<button
type="button"
@click="saveDownloadThreads"
:disabled="saveState.loading"
:class="[
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
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' }}
</button>
</div>
@ -108,27 +110,49 @@
</div>
<TransitionRoot as="template" :show="open">
<Dialog class="relative z-50" @close="open = false">
<TransitionChild as="template" 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
as="template"
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>
<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">
<TransitionChild as="template" enter="ease-out duration-300"
<div
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-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-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
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="mt-3 w-full sm:ml-4 sm:mt-0">
<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">
<button @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">
<button
@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..."
}}
@ -141,25 +165,36 @@
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<LoadingButton :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading"
@click="() => submitDirectory()" :class="[
<LoadingButton
:disabled="currentDirectory == undefined"
type="button"
:loading="createDirectoryLoading"
@click="() => submitDirectory()"
:class="[
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
currentDirectory === undefined
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
: 'text-white bg-blue-600 hover:bg-blue-500',
]">
]"
>
Add
</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"
@click="() => cancelDirectory()" ref="cancelButtonRef">
@click="() => cancelDirectory()"
ref="cancelButtonRef"
>
Cancel
</button>
</div>
<div v-if="error" class="mt-3 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" />
<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">
@ -185,7 +220,6 @@ import {
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
import { Switch } from '@headlessui/vue'
import { type Settings } from "~/types";
const open = ref(false);
@ -197,7 +231,6 @@ const dirs = ref<Array<string>>([]);
const settings = await invoke<Settings>("fetch_settings");
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
const forceOffline = ref(settings?.forceOffline ?? false);
const saveState = reactive({
loading: false,
@ -260,21 +293,21 @@ async function deleteDirectory(index: number) {
await updateDirs();
}
async function saveSettings() {
async function saveDownloadThreads() {
try {
saveState.loading = true;
await invoke("update_settings", {
newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value },
newSettings: { maxDownloadThreads: downloadThreads.value },
});
// Show success state
saveState.success = true;
// Reset back to normal state after 2 seconds
setTimeout(() => {
saveState.success = false;
}, 2000);
} catch (error) {
console.error('Failed to save settings:', error);
} finally {
@ -284,8 +317,8 @@ async function saveSettings() {
function validateNumberInput(event: KeyboardEvent) {
// Allow only numbers and basic control keys
if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault();
}
}

60
pages/settings/index.vue Normal file
View File

@ -0,0 +1,60 @@
<template>
<div class="divide-y divide-zinc-700">
<div class="py-6">
<h2 class="text-base font-semibold font-display leading-7 text-zinc-100">General</h2>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Configure basic application settings
</p>
<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">Start with system</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Drop will automatically start when you log into your computer
</p>
</div>
<Switch
v-model="autostartEnabled"
:class="[
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="[
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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Switch } from '@headlessui/vue'
import { invoke } from "@tauri-apps/api/core";
defineProps<{}>()
const autostartEnabled = ref<boolean>(false)
// Load initial state
invoke('get_autostart_enabled').then((enabled) => {
autostartEnabled.value = enabled as boolean
})
// Watch for changes and update autostart
watch(autostartEnabled, async (newValue: boolean) => {
try {
await invoke('toggle_autostart', { enabled: newValue })
} catch (error) {
console.error('Failed to toggle autostart:', error)
// Revert the toggle if it failed
autostartEnabled.value = !newValue
}
})
</script>

View File

@ -0,0 +1,7 @@
<template>
</template>
<script setup lang="ts">
</script>

2
pages/store/index.vue Normal file
View File

@ -0,0 +1,2 @@
<template></template>
<script setup lang="ts"></script>

View File

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

View File

@ -1,84 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
-ms-overflow-style: none; /* IE and Edge /
scrollbar-width: none; / Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
html::-webkit-scrollbar {
display: none;
}
$motiva: (
("MotivaSansThin.ttf", "ttf", 100, normal),
("MotivaSansLight.woff.ttf", "woff", 300, normal),
("MotivaSansRegular.woff.ttf", "woff", 400, normal),
("MotivaSansMedium.woff.ttf", "woff", 500, normal),
("MotivaSansBold.woff.ttf", "woff", 600, normal),
("MotivaSansExtraBold.ttf", "woff", 700, normal),
("MotivaSansBlack.woff.ttf", "woff", 900, normal)
);
$helvetica: (
("Helvetica.woff", "woff", 400, normal),
("Helvetica-Oblique.woff", "woff", 400, italic),
("Helvetica-Bold.woff", "woff", 600, normal),
("Helvetica-BoldOblique.woff", "woff", 600, italic),
("helvetica-light-587ebe5a59211.woff2", "woff2", 300, normal)
);
@each $file, $format, $weight, $style in $motiva {
@font-face {
font-family: "Motiva Sans";
src: url("/fonts/motiva/#{$file}") format($format);
font-weight: $weight;
font-style: $style;
}
}
@each $file, $format, $weight, $style in $helvetica {
@font-face {
font-family: "Helvetica";
src: url("/fonts/helvetica/#{$file}") format($format);
font-weight: $weight;
font-style: $style;
}
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter/InterVariable.ttf");
font-style: normal;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter/InterVariable-Italic.ttf");
font-style: italic;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-width: 4px;
scrollbar-color: #52525b #00000000;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 4px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: #52525b;
border-radius: 10px;
border: 3px solid #52525b;
}

View File

@ -1,183 +0,0 @@
<template>
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<div class="inline-flex divide-x divide-zinc-900">
<button
type="button"
@click="() => buttonActions[props.status.type]()"
:class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }}
</button>
<Menu
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full">
<MenuButton
:class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-100"
enter-from-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
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"
>
<div class="py-1">
<MenuItem v-if="showOptions" v-slot="{ active }">
<button
@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',
]"
>
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>
</div>
</MenuItems>
</transition>
</Menu>
</div>
</template>
<script setup lang="ts">
import {
ArrowDownTrayIcon,
ChevronDownIcon,
PlayIcon,
QueueListIcon,
ServerIcon,
StopIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
import type { Component } from "vue";
import { GameStatusEnum, type GameStatus } from "~/types.js";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { Cog6ToothIcon, TrashIcon } from "@heroicons/vue/24/outline";
const props = defineProps<{ status: GameStatus }>();
const emit = defineEmits<{
(e: "install"): void;
(e: "launch"): void;
(e: "queue"): void;
(e: "uninstall"): void;
(e: "kill"): void;
(e: "options"): void;
(e: "resume"): void;
}>();
const showDropdown = computed(
() =>
props.status.type === GameStatusEnum.Installed ||
props.status.type === GameStatusEnum.SetupRequired ||
props.status.type === GameStatusEnum.PartiallyInstalled
);
const showOptions = computed(
() => props.status.type === GameStatusEnum.Installed
);
const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]:
"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 hover:bg-zinc-700",
[GameStatusEnum.Downloading]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Validating]:
"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 hover:bg-yellow-500",
[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",
[GameStatusEnum.PartiallyInstalled]:
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
};
const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Install",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading",
[GameStatusEnum.Validating]: "Validating",
[GameStatusEnum.SetupRequired]: "Setup",
[GameStatusEnum.Installed]: "Play",
[GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume",
};
const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
[GameStatusEnum.Queued]: QueueListIcon,
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
[GameStatusEnum.Validating]: ServerIcon,
[GameStatusEnum.SetupRequired]: WrenchIcon,
[GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: StopIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
};
const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.Remote]: () => emit("install"),
[GameStatusEnum.Queued]: () => emit("queue"),
[GameStatusEnum.Downloading]: () => emit("queue"),
[GameStatusEnum.Validating]: () => emit("queue"),
[GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {},
[GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
};
</script>

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