Compare commits

..

108 Commits

Author SHA1 Message Date
124d51bced fix: potential download fixes 2025-08-05 22:05:29 +10:00
3b830e2a44 Move frontend to main folder (#109)
* feat: small refactor

* fix: appimage build script

* fix: add NO_STRIP to AppImage build

* fix: build and dev mode from refactor

* fix: submodule step 1

* fix: submodules step 2
2025-08-05 16:09:47 +10:00
75a4b73ee1 QoL Download Manager (#108)
* feat: retry specific download errors

* fix: potential fix for cmd window on Windows

* feat: add disk space check for download

* fix: update game fix formatting

* fix: clippy
2025-08-04 16:30:45 +10:00
339d707092 Fix errors with caching when cache is deleted (#101) 2025-08-04 15:02:32 +10:00
776dc8fe7a Fixes reqwest client setup, #87 (#107) 2025-08-04 15:01:44 +10:00
dbe8c8df4d Process manager templating & game importing (#96)
* feat: add new template options, asahi support, and refactoring

* feat: install dir scanning, validation fixes, progress fixes, download manager refactor

This kind of ballooned out of scope, but I implemented some much
needed fixes for the download manager.

First off, I cleanup the Downloadable trait, there was some
duplication of function.

Second, I refactored the "validate" into the GameDownloadAgent,
which calls a 'validate_chunk_logic' yada, same structure as
downloading.

Third, I fixed the progress and validation issues.

Fourth, I added game scanning

* feat: out of box support for Asahi Linux

* fix: clippy

* fix: don't break database
2025-08-02 20:17:27 +10:00
35f49b8811 macOS app signing (#95)
* feat: add macos signing args

* fix: update all versions to -mac specific

* fix: fetch signing identity

* feat: add signing pre-steps like the docs say

* fix: remove apple requirement from signing

* fix: add drop cert to keychain when signing

* fix: add drop.pem to add-trusted-cert

* fix: re-order and specify import operation

* fix: let's try the user store

* fix: password required to update trust

* fix: try another non-interactive fix

* fix: try sudo

* fix: revert attempt fix

* fix: add cert id debug

* fix: attempt to use id rather than name

* fix: revert code id to name
2025-08-02 15:01:53 +10:00
cc5339a389 Reqwest optionally load certificates from disk (#94)
* feat: Add get_client function

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

* chore: Converted all instances of reqwest::blocking::Client::new() and reqwest::Client::new() to DROP_CLIENT_SYNC and DROP_CLIENT_ASYNC respectively

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

* fix: use_remote_logic not using certificates

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

* fix: add log statement to certificates

* chore: add more logging

* fix: clippy

* refactor: into single fetch_certificates func

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-02 11:59:50 +10:00
6104bfda72 Bump version to v0.3.1 (#85) 2025-08-01 14:13:13 +10:00
be688cb18f Version bump: v0.3.0 2025-08-01 14:09:16 +10:00
13cc69f10e Device code authorization (#83)
* feat: device code authorization

* Fix for setup executable unable to be launched (#81)

* Fix for redownload invalid chunks (#84)

* feat: Redownloading invalid chunks

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

* fix: clippy

* fix: clippy x2

---------

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

* chore: Run clippy fix pedantic

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

* feat: add better error handling

* fix: clippy

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-01 13:12:05 +10:00
574782f445 chore: Run clippy fix pedantic
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-01 08:42:45 +10:00
b5a8543194 Fix for redownload invalid chunks (#84)
* feat: Redownloading invalid chunks

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

* fix: clippy

* fix: clippy x2

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-07-31 18:25:38 +10:00
d0e4aea5ce Fix for setup executable unable to be launched (#81) 2025-07-30 09:57:07 +10:00
739e6166c5 Cache-first object fetching (#76)
* fix: submillisecond cache hits

* fix: async object loading to hand control back to renderer

* fix: clippy
2025-07-27 12:04:50 +10:00
682c6e9c0b Bump version to v0.3.0-rc-8 (#74) 2025-07-25 22:21:59 +10:00
46e1f16cdd Process manager fixes (#71)
* fix: launching on linux

* feat: #70

* feat: add dummy store page

* feat: add store redir and refresh button to library

* feat: cache first object fetching

* feat: Remove let_chains feature and update to Rust 2024

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

* feat: Check for if process was manually stopped

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

* fix: use bitcode instead of serde

* chore: remove logs

* fix: clippy

* fix: clippy 2

* fix: swap to stop icon

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-07-25 10:44:40 +10:00
d19f9bbc31 Fix client running behind reverse proxy (#69)
* fix: reverse proxy 400 due to duplicate header

* fix: clippy

* bump version and update ci
2025-07-18 20:08:12 +10:00
2913fdf35b Release v0.3.0-rc-6 (#68) 2025-07-18 17:38:36 +10:00
f9fdf151ea Clippy CI/CD (#67)
* feat: add clippy ci

* fix: clippy errors

* fix: ci/cd

* fix: update ci packages

* fix: add gtk3 to ci deps

* fix: add webkit to ci deps

* fix: ci deps and perms

* fix: add clippy settings to lib.rs
2025-07-18 17:36:04 +10:00
495d93705e Panic hook to generate crash dumps #65 (#66) 2025-07-18 16:35:02 +10:00
c477dd4872 Fix windows build by removing linux extension import (#64) 2025-07-14 16:43:11 +10:00
f560a62c8f Download fixes (#63)
* refactor: Rename StoredManifest to DropData

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

* fix: Downloads when resuming would truncate files which had not been finished

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

* chore: Didn't import debug macro

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

* fix: Download chunks with wrong indexes

Migrated to using checksums as indexes instead

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

* feat: Resume download button

Also added DBWrite and DBRead structs to make database management easier

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

* feat: Download resuming

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

* feat: Resume button and PartiallyInstalled status

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

* feat: Download validation

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

* chore: Ran cargo fix & cargo fmt

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

* fix: download validation, installs, etc

* chore: version bump

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-07-14 16:31:06 +10:00
2874b9776b fix: Accidentally moved request when setting the header
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-25 09:17:06 +10:00
afcd4e916f chore: bump version to 0.3.0-rc-4 2025-06-25 09:05:08 +10:00
885fa42ecc fix: Move Authorization header generation to download_game_chunk()
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-25 06:53:42 +10:00
6d295bd47f fix: Broken README path
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:37:14 +10:00
c3ee09af85 fix: Update broken README link in docs
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:35:57 +10:00
0ce55e12c5 fix: Re-update the user and app status when recieve_handshake is called (#54)
Also enabled assetProtocol for better caching in general

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-06 12:09:44 +10:00
86bce1c68d Release: v0.3.0-rc-3 (#51) 2025-06-06 09:25:44 +10:00
924e4e334c Database not being properly serialised with rpm_serde (#48)
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-05 17:22:22 +10:00
065eb2356a fix: database corrupted on every startup (#40) 2025-06-01 19:53:24 +10:00
689e9ad890 fix: add new dependencies to linux build 2025-05-28 20:51:32 +10:00
7c35ed73aa fix: Folders can now be copied too
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
8f261a5dac chore: Add extract() function
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
67b6f2aa2e chore: Initial path normalisation & parsing with backup generation
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:47:43 +10:00
11e2b3fe8a fix: regenerate lockfile 2025-05-28 20:37:26 +10:00
eba224f998 fix: remove memd-exec dependency 2025-05-28 20:22:18 +10:00
d045385a5d build: 0.3.0-rc-2 2025-05-28 20:09:58 +10:00
d878806ade Merge branch 'compat' into develop
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 11:24:30 +10:00
b71081006e refactor: Reorganise file structure
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 11:19:48 +10:00
c9e1ed78eb refactor: Delete downloadable_metadata.rs
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 09:18:33 +10:00
446aa70b0b inprogress: compat 2025-05-28 09:07:09 +10:00
1d0b81078a feat: Add "NO_TRAY_ICON" env option
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-27 12:13:49 +10:00
5251a56c3c feat: add arm linux builds 2025-05-25 11:46:24 +10:00
eeca8a7a98 chore(tailscale): Add test
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-16 15:01:50 +10:00
365cdaf311 feat(tailscale): Add TailscaleListener and TailscaleConn
Needs testing on a native windows machine

Signed-off-by: quexeky <git@quexeky.dev>
2025-05-16 12:57:11 +10:00
2957773179 fix: remove reqwest default-features to compile without openssl 2025-05-15 21:03:31 +10:00
15e5fe4dc0 fix: move to reqwest rustls 2025-05-15 20:58:48 +10:00
2dc0a78354 fix: update cargo lock 2025-05-15 20:53:04 +10:00
51c480f245 feat: inline capability registration 2025-05-15 16:05:34 +10:00
95d223e2b2 feat(tailscale): Add wrapper around libtailscale with Tailscale struct
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-15 15:23:20 +10:00
790e8c2afe feat: move to native_model to allow for database upgrades 2025-05-15 10:13:24 +10:00
02edb2cbc1 chore: libtailscale rust build 2025-05-14 10:01:26 +10:00
4b4c0734ec fix: windows builds 2025-05-10 16:41:37 +10:00
e75e0044fb fix: windows launching 2025-05-10 15:38:20 +10:00
65561abdab fix: update object id paths for new server 2025-05-10 15:25:40 +10:00
fed3e08dce fix: re-add minimise button 2025-05-10 14:22:21 +10:00
b0b1e397b1 fix: install dir flow 2025-05-10 09:02:59 +10:00
7b443818d1 fix: github ci and dialogtitle issue 2025-05-08 08:25:10 +10:00
fa4a881cc0 fix: bump tauri.config.json to 0.3.0-rc-1 2025-05-07 14:36:54 +10:00
4f16a6e6b2 fix: remove nightly trigger
it creates releases
2025-05-07 14:29:27 +10:00
47d9e9949b feat: bump app versions to 0.3.0-rc-1 2025-05-07 14:28:11 +10:00
a643d6102b fix: switch to rust nightly 2025-05-07 13:51:27 +10:00
a71ff160c2 feat: add github build 2025-05-07 12:21:03 +10:00
a53a566792 feat: cleanup settings menu and fix styles 2025-05-01 13:36:52 +10:00
ac6b034501 fix: error with game options for remote games 2025-05-01 12:26:41 +10:00
5ef20f7a57 chore(library): Update error type on update_game_configuration 2025-04-28 11:31:54 +10:00
8e5e3b2715 fix: some of GitHub's dependabot alerts 2025-04-27 21:15:32 +10:00
0f717d51d0 feat: launch options 2025-04-27 21:07:39 +10:00
4941f2a6fa feat: better error message if cannot connect to provided url 2025-04-26 01:06:03 +10:00
40eb19cf8b feat: add iframe store page 2025-04-08 16:17:03 +10:00
6b9b9e3606 feat: add backend for template launching 2025-04-07 13:52:52 +10:00
3e074abc0a feat: improve errors and include installed games in library 2025-04-05 15:36:53 +11:00
1fdf569278 fix: offline game status, user widget and use binary-encoding 2025-04-04 11:07:10 +11:00
77251a6524 feat: better client name w/ hostname 2025-04-04 10:14:23 +11:00
137b71b3ba feat: switch to shell-based command launching
note: needs error handling
2025-04-02 20:04:14 +11:00
569ba4243c feat: add offline widget & remove openssl in favour of droplet-rs 2025-04-02 11:00:39 +11:00
834f52d024 fix: macos and ui 2025-03-15 15:05:35 +11:00
1ce6be80db fix(collections): Ensured that all internal collection commands use and send the correct data
Signed-off-by: quexeky <git@quexeky.dev>
2025-03-11 20:35:43 +11:00
19c8fc24aa chore(collections): Slightly fixed return value for collections
Signed-off-by: quexeky <git@quexeky.dev>
2025-03-11 19:26:05 +11:00
4239215451 feat(collections): Added all internal collections commands
Signed-off-by: quexeky <git@quexeky.dev>
2025-03-11 12:34:56 +11:00
9614af7f03 feat(collections): Added fetch_collections function
Signed-off-by: quexeky <git@quexeky.dev>
2025-03-11 10:46:16 +11:00
639d3b4630 fix: refactoring and error handling 2025-02-20 21:19:54 +11:00
cdcd69391d Merge remote-tracking branch 'aden/develop' into develop 2025-02-18 14:45:09 +11:00
8520b255a3 style(library): Re-designed Library UI with new features 2025-02-15 16:41:32 +10:30
d9c4f7aa75 feat(library): Reactive library updating
Signed-off-by: quexeky <git@quexeky.dev>
2025-02-12 10:00:45 +11:00
316a3742eb fix(cache): Added proper error handling to fetch_object(_offline) 2025-02-12 10:00:45 +11:00
b9df197534 feat(cache): Caching objects which use the useObject tauri command 2025-02-12 10:00:45 +11:00
5c479cb283 chore(cache): Added fetch_drop_object command
Signed-off-by: quexeky <git@quexeky.dev>
2025-02-12 10:00:45 +11:00
4c59c5d6c1 feat(cache): Implemented caching for game metadata 2025-02-12 10:00:45 +11:00
9977107374 fix(cache): Bug where games would not remove themselves from the list of installed applications when being uninstalled 2025-02-12 10:00:45 +11:00
2690c3019d chore: Various formatting 2025-02-12 10:00:45 +11:00
2a1a7326d0 feat(cache): Added forceOffline in settings and caching games & library 2025-02-12 10:00:45 +11:00
f33ca95bdf feat(cache): Added offline!() macro to manage online and offline function distinctions
See fetch_library command for example
2025-02-12 10:00:45 +11:00
bb23e88ead chore: Swapped over to using a macro with an offline mode
Signed-off-by: quexeky <git@quexeky.dev>
2025-02-12 10:00:45 +11:00
810fbdfe49 chore: Progress on caching 2025-02-12 10:00:45 +11:00
e204ff30b4 fix: Removed unnecessary nightly feature try_trait_v2 2025-02-12 10:00:45 +11:00
501145c5d9 fix(downloads): Fix rearranging download queue throwing error 2025-02-12 10:00:45 +11:00
dca5f65e89 chore: Version bump 2025-02-12 10:00:45 +11:00
00f55ff3ae Merge branch 'main' into develop 2025-01-25 18:46:33 +11:00
52c70052a4 Update changelog.md 2025-01-25 14:35:17 +11:00
7a0cf4fbb6 fix(logging): Restored RUST_LOG env functionality 2025-01-25 14:34:08 +11:00
76bae3d926 fix(library): Added "LIbrary Failed to Update" content to recover from library load fail 2025-01-24 22:35:09 +11:00
53234d283e feat(settings): Made save button include user feedback & only allow numeric characters 2025-01-24 13:01:59 +11:00
6e4ac4ad83 Update issue templates 2025-01-21 10:17:46 +11:00
40c4e8a71c Update .gitlab-ci.yml file 2025-01-21 10:17:46 +11:00
4ad688da14 refactor(downloads): Replaced static usage with const 2025-01-21 10:17:30 +11:00
185 changed files with 17914 additions and 9214 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Arch Linux, Windows]
- App Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

23
.github/workflows/clippy.yml vendored Normal file
View File

@ -0,0 +1,23 @@
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

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

@ -0,0 +1,104 @@
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
# 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.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
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 }}

3
.gitignore vendored
View File

@ -27,3 +27,6 @@ dist-ssr
src-tauri/flamegraph.svg src-tauri/flamegraph.svg
src-tauri/perf* src-tauri/perf*
/*.AppImage
/squashfs-root

View File

@ -3,7 +3,7 @@ stages:
build-linux: build-linux:
stage: build stage: build
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly
script: script:
- apt-get update -y - apt-get update -y
- apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y - apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[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

@ -4,7 +4,7 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It u
## Running ## Running
Before setting up the drop app, be sure that you have a server set up. 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) The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
## Current features ## Current features
Currently supported are the following features: Currently supported are the following features:

48
build.mjs Normal file
View File

@ -0,0 +1,48 @@
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 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

@ -1,5 +1,292 @@
## Release 0.2.0-beta
### Fixes
- Re-enabled killing games #005bab2
- fixed queue manipulation and waiting for downloads #01260f0
- fix logic error in detecting dir #04368ff
- absolute executable invoke #17759c4
- don't crash download manager if multiple errors come in #21204de
- clear stale data before requesting new #327628b
- fixed completed indexes #39f2ebd
- add file & line to console logs #4d8eadc
- Games not launching due to string semantics #4ef49cc
- Added error handling for chunk request errors #4fc0855
- Chunk counting logic error #5ba151f
- modal stack doesn't cover whole app #5db9ae5
- use set_file_name instead of pushing to strings #60d0a48
- use of completed signal, and pause/resuming #64d7f64
- add message about nonce expiration #6a8d0af
- Added "LIbrary Failed to Update" content to recover from library load fail #76bae3d
- Restored RUST_LOG env functionality #7a0cf4f
- initialise doesn't recreate default install dir #7a3841b
- update routes for new server #7ab53f3
- use vendored flag #7c8089e
- fix poorly designed parsing for executables with spaces #7c90d2b
- assorted fixes #89ea34c
- Added Settings component #8aad64f
- windows build #8d9234f
- fix ugly scrollbars on edge webview #95f2174
- windows shadow #9a8cc59
- add better error message #9af0d08
- Broken command invoke logic in settings/downloads.vue #9e29aa7
- Accidentally was attempting to lock onto something that was already in scope #9e82a0b
- fix incorrect error assumptions & update types #a17311a
- Re-enabled uninstalling apps #a56ee25
- types #af056c0
- fix other metadata endpoints #c2f54c1
- Re-enabled deep links #c3f6222
- added console as an appender #d12bf15
- remove unnecessary unstable feature #d5ac1b0
- fix install button #d7b0302
- stop loading on error #d83aae6
- use unix timestamp to avoid invalid characters in filename #dafce24
- Renamed game_id to id #dceaa56
- use chrono library to generate timestamps #e22e6d8
- clear stale data before requesting new #e72662c
- fix scrollbars on edge webview #f09605a
- update readme instructions #f0c47d8
- Adding usize to completed_contexts_lock instead of &usize #f508186
### Features
- Game kill tauri command #01e6162
- add debug page #02f8591
- Add signout functionality (#16) #0a0d9d6
- queue and library UIs #0a20139
- add note about more install dirs #139bc0c
- Using SerializeDisplay for better error management with Result #170fde5
- add pre-launch log to file #17f8d76
- Added option to change root directory #1aa52c0
- add speed and time remaining information #1f899ec
- lockless tracking of downloaded chunks #2183585
- quit button #239b8d5
- use shift or DEBUG RUST_LOG to show Debug Info #245a84d
- Added database corruption dialog #25ba200
- only allow downloads for supported platforms #269dcbb
- add installed ui in the library menu #2c8164e
- added file-based logging #2d4a7e8
- automatically fetch remote data if not available #2dedfbb
- Added database recovery #32ae7d5
- ability to add more download dirs #384f7a5
- re-enable checksums #3ca87fc
- background processes and close/open menu #3d60fd5
- launch games with log files #3f71149
- Download cancelling #450bca9
- refactoring and error message #469a2d6
- Added UI to change download threads #4e93eb4
- Made save button include user feedback & only allow numeric characters #53234d2
- download widget and queue fix #532d13e
- Pausing and resuming game downloads #55b7921
- Allow settings to update UI using fetch_settings command #5bb04da
- temporary queue ui and flamegraph instructions #5cbeb3b
- Added DownloadThreadControl struct #5e05e68
- Added max_download_threads setting and separated settings from db #5ea47d7
- Added generic download manager #6159319
- Added AgentInterfaceData to get information about all downloads in queue #63c3cc1
- debug queue interface #671d45f
- reduce scope of download agent #6a38ea3
- Added multi-argument game launch and setup support #6ad3837
- shared child with stop command #6b96e40
- Added function to take and set any game state #6bc6482
- Added line numbers to file logging and highlighting to console #7c3140e
- Separated chunk updates into individual counters #7d3c601
- Ensure that any database issues are resolved by standalone functions #7d4651d
- ui to install games #8670bca
- Implemented spawning with umu (using umu-wrapper-lib) #88b2505
- offer manual signin #949acfc
- better process management, including running state #a135b13
- Added Download Manager #a1ada07
- retry connnection on server unavailable #a53d838
- finish download dir CRUD interface #a580a46
- better download manager errors + modal #ad92dbe
- syncs state to disk to persist across reboots #b556842
- prevent default context menu and emit event on elements #c560656
- initial creation and logo update #d9a51cf
- Added manifest.json utility for persistent download progress #d9d0122
- game uninstalling & partial compat #dd7f567
- combined db and download interface improvements #de52dac
- update db state with ui and emit events #e4df4eb
- Generic function to set download state #f10d92d
- Convert DownloadThreadControlFlag to AtomicBool #f25bfed
- add note about more install dirs #f4ac1c8
- Added rolling progress window #fd30b3e
### Other Changes
- quexeky <git@quexeky.dev>
- Convert DATA_ROOT_DIR to Mutex #00b7179
- Converting DB access to a trait #01b092c
- Updated changelog #022330b
- Progress on cleanup and exit #0381b8b
- library ui #03fa364
- Scoping changes and removing qualifications #046ba64
- Moved all files relevant to game downloads to their own directory #06d1e9e
- SLowly integrating game_download into the FE. Started with using the manifest minimal example in the server (#1) #07379b2
- Ran cargo clippy & moved DownloadManagerInterface #075d6ec
- Made logging systems match #0a1dddf
- Some easy cleanup of the download manager #0a2ac25
- client now fetches user information from Drop server #0c0cfeb
- Included in AppStatus (Also trying to link to Issue #1)
- Accidentally serialized AppStatus and broke everything :/ #10791ed
- Removed debugging statements #10c8344
- Wrappers are the bane of my existence. Also here's the download cancelling logic. #13df631
- Merge branch 'error-handling' #1520471
- Updated README.md #165a967
- Removed unnecessary dependencies #1724449
- merge(download-manager) -> 'main' #172d6b0
- More refactoring and renaming camelCase struct definitions to snake_case #1742793
- General cleanup #182361e
- Delete pages/library.vue #1861659
- progress on more precise download control #18b9149
- Allowing some dead code features because they are there for future use (potentially) #191e62c
- Ensure that Downloadable is also send and sync #1a89135
- I think that download queuing is working #1ab61c8
- auth initiate, database and more #22b1aee
- Update .gitlab-ci.yml" #2307704
- More fleshing out on how specifically game downloads will work (#1) #23137dd
- Removed utils.rs #270bc8b
- Fixing some references to "id" vs "game_id" #27e5a8e
- More cleanup after cargo clippy #2822b7a
- Updated contributing link #2aa5b9c
- More fleshing out on how specifically game downloads will work #2b90de9
- Cleaning up downloads playing and pausing #2c7b5fb
- fixed multi-chunk downloads #2ec351f
- Clippy refactoring #2efe304
- remove unpacker mod statement #32067c0
- Progress on adding tools #3299c71
- Fixed bug with bad initial loading into store instead of auth #3923acf
- add nvm rc #3ccd444
- partial download manager #3dbf5ab
- Update .gitlab-ci.yml with artifacts #3e10f17
- Removed tools/ #3eda979
- Downloads should be fixed now #403ca65
- transient vs synced state now defined #42c0198
- added adenmgb's autostart feature #472eb1d
- better download defaults #4779383
- Progress on downloads. Currently working on parsing functions to be run asynchronously #496c6a5
- Ran cargo clippy & cargo fmt #4983b25
- handshakes #4bb33c8
- Convert DOWNLOAD_MAX_THREADS to const #4fc13a1
- Merge branch 'downloads' #50ed841
- Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs #50f37fd
- Added time debugging and fixed logging formatting #5243694
- Clippy changes #553bc37
- Queue is running game downloads sequentially now #5564d23
- migrate to new droplet ca system #556898f
- Add LICENSE #57a5737
- ran cargo clippy & cargo fmt #5e3d26b
- my own take on some BASED design decisions #5ed0833
- cleanup and game UI beginnings #5ef6b8e
- Progress on terminator #5f5cbd0
- Implement better error system and segregate errors and commands (#23) #604d5b5
- moved to completed index arr to help serialization #64ebc19
- Ran cargo clippy & cargo fmt #653717e
- Removed all references to anything outside of the DownloadManager #6568faa
- Merge remote-tracking branch 'origin/main' #68ca4a7
- swap file name and to binary encoding #694f2fd
- chore(polish & cleanup) #6cc0c67
- Update .gitlab-ci.yml #6d7630e
- Moved some variable declarations outside of the spawned download thread #6ea4cf2
- Encoding game IDs and versions #6ef444e
- restructing and renaming #7049673
- Converted to md5 #706f525
- Merge branch 'main' into downloads #714b968
- Semantic naming changes #725f16b
- Abstracted queue system #76b0975
- Moved manifest and stored_manifest to download_manager" #78149bb
- README update #78fc668
- Ensured everything is serializing/deserializing to camelCase #7a95b7f
- fixed some of quexeky's BASED design decisions #7e3da04
- Progress checker works #7fec00d
- Progress on refactoring and abiding by cargo clippy #816b427
- Added GAME_PAUSE_CHECK_INTERVAL value #8204795
- Ran cargo clippy & fmt #82804eb
- update metadata #85a0899
- Renamed most instances of "game" outside of actual game downloads #881fcc6
- Debugging & starting work on parsing manifest #89d2814
- slight ui/ux fixes and updates to auth protocol #8a2d23d
- Removed Arc requirement for DownloadableMetadata #8be1dd4
- compliant with new APIs #8f6f184
- Ran cargo clippy & cargo fmt #9272970
- Added rolling_progress_updates.rs #9369ff1
- Add files via upload #93b8b83
- More refining info!() statements #94cf678
- fixed windows issues #959dad3
- Starting p2p progress #97bb1fa
- Game downloads from the client are working (multithreaded) by parsing in gameID, GameVersion, and maxThreads from FE (#1) #984472e
- Version bump & appimage build #9897698
- Some progress on thread terminations #99beca4
- rename files to what they contain #99c8b39
- Created separate function to generate requests #9a184a8
- cleanup of lib and toml #9b1cfa7
- refactor for generic way to implement cross platform launchers #9ea2aa4
- Updated logging format #a213765
- fix(windows build) #a24cc8a
- Added ToolDownloadAgent #a2e63aa
- copy direct to disk #a628fc1
- Moved manifest and stored_manifest to download_manager #a846eed
- adds nvm rc! #a881d8e
- Reordered DownloadThreadControlFlag to agree with From<bool> #ab606e8
- ci/cd and patches for windows builds #ac1c3b6
- patch for not draggable windows during setup #ac66b20
- another stage of client authentication #ae4c65b
- Renamed GameDonwloadError to ApplicationDownloadError and moved #aed58e4
- Progress on write speeds & added debug statements #b065e10
- Updated logging #b3963b6
- Created file settings.rs #b47b7ea
- Added Downloadable trait and replaced references to GameDownloadAgent #b4d70a3
- Update .gitlab-ci.yml #b6a54c0
- Moved download manager to separate directory #b6c64e5
- Ran cargo fmt #b8cf44c
- Imported appropriate logging macros #b99ff67
- Merge branch 'main' into download-manager #bb60942
- Ran cargo clippy & cargo fmt #bd3deac
- beginnings of game state management #bf46dec
- Update Cargo.toml #c1fb39e
- migrated unpacking to rust zstd to conform with droplet #c46c54b
- More progress on checksums #c51e761
- Delete pages/library.vue #c722a54
- Merge branch 'downloads' (again) #c748aec
- migrate to nuxt and groundwork #c957744
- More debugging because apparently checksums are the bane of my existence. But it works and I was just an idiot #c9d9d2e
- Fully separate & generic download manager #cac612b
- Progress on rolling progress window #cf19477
- Ensured that all logs start with lowercase capital and have no trailing punctuation #cfc9d13
- Validated that loading data works #d21b1d2
- Mostly finished with checksums. Just merging main in at the same time #d39e7cb
- Ran cargo clippy #dcb1564
- Add files via upload #dcb2c0f
- Theoretically adding queue support and optimistic manifest downloading (#1). Needs tests when actual functions are implemented #dcd8fa8
- Merge remote-tracking branch 'origin/downloads' into downloads #dd23ca8
- Debugging line #ddc585d
- Re-enabled closing the window and some more renaming #defba51
- drop no longer freaks out if server is unavailable on startup #df88395
- Apply stashed changes #e0ea8c9
- Merge remote-tracking branch 'origin/downloads' into downloads #e4e605b
- convert to more sensible permission schema #e504c00
- Update on GameDownload #e71e4cf
- reorganisation, cleanup and new nonce protocol #e828bca
- rustix fs feature #e9805a8
- Added manage_go_signal command #ea70ec9
- Drop will no longer crash when the server goes down #eb3311a
- Made all errors type-based #ec2f414
- Added description on how the DownloadManager works #f029cbf
- Using more appropriate logging statements #f183a9d
- remove unnecessary compat code (#20) #f1c8bbf
- Manifests are now being parsed successfully #f28c880
- Removed tests/ #f29e989
- I think that downloads are working. Need to test and set decent file locations now #f388237
- Just debugging tauri's damn Sync command features #f60ca2b
- fixes and patches for merged changes #f6476bc
- Added manage_queue_signal #f64782e
- initial commit #f6cd7c3
- Update .gitlab-ci.yml #fc6bab9
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.1.0-beta ## Release 0.1.0-beta
### Fixes ### Fixes

View File

@ -1,108 +0,0 @@
<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

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

View File

@ -1,12 +0,0 @@
<template>
<div class="flex flex-col bg-zinc-900 overflow-hidden">
<Header class="select-none" />
<div class="relative grow overflow-y-auto">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
const queueState = useQueueState();
</script>

1
libs/drop-base Submodule

Submodule libs/drop-base added at 04125e89be

View File

@ -1,4 +1,5 @@
<template> <template>
<LoadingIndicator />
<NuxtLayout class="select-none w-screen h-screen"> <NuxtLayout class="select-none w-screen h-screen">
<NuxtPage /> <NuxtPage />
<ModalStack /> <ModalStack />
@ -9,8 +10,6 @@
import "~/composables/downloads.js"; import "~/composables/downloads.js";
import { invoke } from "@tauri-apps/api/core"; 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 { useAppState } from "./composables/app-state.js";
import { import {
initialNavigation, initialNavigation,
@ -20,18 +19,26 @@ import {
const router = useRouter(); const router = useRouter();
const state = useAppState(); const state = useAppState();
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
router.beforeEach(async () => { async function fetchState() {
try { try {
state.value = JSON.parse(await invoke("fetch_state")); 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) { } catch (e) {
console.error("failed to parse state", 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(); setupHooks();

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -0,0 +1,31 @@
<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

@ -0,0 +1,122 @@
<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

@ -0,0 +1,183 @@
<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>

View File

@ -11,7 +11,7 @@
v-for="(nav, navIdx) in navigation" v-for="(nav, navIdx) in navigation"
:class="[ :class="[
'transition uppercase font-display font-semibold text-md', 'transition uppercase font-display font-semibold text-md',
navIdx === currentPageIndex navIdx === currentNavigation
? 'text-zinc-100' ? 'text-zinc-100'
: 'text-zinc-400 hover:text-zinc-200', : 'text-zinc-400 hover:text-zinc-200',
]" ]"
@ -28,9 +28,7 @@
/> />
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<ol class="inline-flex gap-3"> <ol class="inline-flex gap-3">
<HeaderQueueWidget <HeaderQueueWidget :object="currentQueueObject" />
:object="currentQueueObject"
/>
<li v-for="(item, itemIdx) in quickActions"> <li v-for="(item, itemIdx) in quickActions">
<HeaderWidget <HeaderWidget
@click="item.action" @click="item.action"
@ -39,21 +37,23 @@
<component class="h-5" :is="item.icon" /> <component class="h-5" :is="item.icon" />
</HeaderWidget> </HeaderWidget>
</li> </li>
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
<HeaderUserWidget /> <HeaderUserWidget />
</ol> </ol>
</div> </div>
</div> </div>
<WindowControl class="h-16 w-16 p-4" /> <WindowControl />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid"; import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid";
import type { NavigationItem, QuickActionNav } from "../types"; import { AppStatus, type NavigationItem, type QuickActionNav } from "../types";
import HeaderWidget from "./HeaderWidget.vue"; import HeaderWidget from "./HeaderWidget.vue";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
const window = getCurrentWindow(); const window = getCurrentWindow();
const state = useAppState();
const navigation: Array<NavigationItem> = [ const navigation: Array<NavigationItem> = [
{ {
@ -78,7 +78,7 @@ const navigation: Array<NavigationItem> = [
}, },
]; ];
const currentPageIndex = useCurrentNavigationIndex(navigation); const { currentNavigation } = useCurrentNavigationIndex(navigation);
const quickActions: Array<QuickActionNav> = [ const quickActions: Array<QuickActionNav> = [
{ {

View File

@ -0,0 +1,5 @@
<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,5 +1,5 @@
<template> <template>
<Menu v-if="state.user" as="div" class="relative inline-block"> <Menu v-if="state?.user" as="div" class="relative inline-block">
<MenuButton> <MenuButton>
<HeaderWidget> <HeaderWidget>
<div class="inline-flex items-center text-zinc-300 hover:text-white"> <div class="inline-flex items-center text-zinc-300 hover:text-white">
@ -23,7 +23,7 @@
<MenuItems <MenuItems
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md" class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
> >
<PanelWidget class="flex-col gap-y-2"> <div class="flex-col gap-y-2">
<NuxtLink <NuxtLink
to="/id/me" to="/id/me"
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800" class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
@ -49,7 +49,10 @@
Admin Dashboard Admin Dashboard
</a> </a>
</MenuItem> </MenuItem>
<MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active, close }"> <MenuItem
v-for="(nav, navIdx) in navigation"
v-slot="{ active, close }"
>
<button <button
@click="() => navigate(close, nav)" @click="() => navigate(close, nav)"
:href="nav.route" :href="nav.route"
@ -58,11 +61,11 @@
'transition text-left block px-4 py-2 text-sm', 'transition text-left block px-4 py-2 text-sm',
]" ]"
> >
{{ nav.label }}</button {{ nav.label }}
> </button>
</MenuItem> </MenuItem>
</div> </div>
</PanelWidget> </div>
</MenuItems> </MenuItems>
</transition> </transition>
</Menu> </Menu>
@ -80,12 +83,12 @@ const open = ref(false);
const router = useRouter(); const router = useRouter();
router.afterEach(() => { router.afterEach(() => {
open.value = false; open.value = false;
}) });
const state = useAppState(); const state = useAppState();
const profilePictureUrl: string = await invoke("gen_drop_url", { const profilePictureUrl: string = await useObject(
path: `/api/v1/object/${state.value.user?.profilePicture}`, state.value?.user?.profilePictureObjectId ?? ""
}); );
const adminUrl: string = await invoke("gen_drop_url", { const adminUrl: string = await invoke("gen_drop_url", {
path: "/admin", path: "/admin",
}); });
@ -96,11 +99,6 @@ function navigate(close: () => any, to: NavigationItem){
} }
const navigation: NavigationItem[] = [ const navigation: NavigationItem[] = [
{
label: "Account settings",
route: "/account",
prefix: "",
},
{ {
label: "App settings", label: "App settings",
route: "/settings", route: "/settings",
@ -110,6 +108,6 @@ const navigation: NavigationItem[] = [
label: "Quit Drop", label: "Quit Drop",
route: "/quit", route: "/quit",
prefix: "", prefix: "",
} },
] ];
</script> </script>

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<template></template>
<script setup lang="ts">
const loading = useLoadingIndicator();
watch(loading.isLoading, console.log);
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { data } from "autoprefixer"; import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types"; import { AppStatus, type AppState } from "~/types";
export function setupHooks() { export function setupHooks() {
const router = useRouter(); const router = useRouter();
const state = useAppState();
listen("auth/processing", (event) => { listen("auth/processing", (event) => {
router.push("/auth/processing"); router.push("/auth/processing");
@ -15,8 +17,9 @@ export function setupHooks() {
); );
}); });
listen("auth/finished", (event) => { listen("auth/finished", async (event) => {
router.push("/store"); router.push("/library");
state.value = JSON.parse(await invoke("fetch_state"));
}); });
listen("download_error", (event) => { listen("download_error", (event) => {
@ -27,12 +30,31 @@ export function setupHooks() {
description: `Drop encountered an error while downloading your game: "${( description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string event.payload as unknown as string
).toString()}"`, ).toString()}"`,
buttonText: "Close" buttonText: "Close",
}, },
(e, c) => c() (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) => { document.addEventListener("contextmenu", (event) => {
@ -43,7 +65,13 @@ export function setupHooks() {
*/ */
} }
export function initialNavigation(state: Ref<AppState>) { export function initialNavigation(state: ReturnType<typeof useAppState>) {
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: "App state not valid",
fatal: true,
});
const router = useRouter(); const router = useRouter();
switch (state.value.status) { switch (state.value.status) {
@ -60,6 +88,6 @@ export function initialNavigation(state: Ref<AppState>) {
router.push("/error/serverunavailable"); router.push("/error/serverunavailable");
break; break;
default: default:
router.push("/store"); router.push("/library");
} }
} }

91
main/error.vue Normal file
View File

@ -0,0 +1,91 @@
<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>

82
main/layouts/default.vue Normal file
View File

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

View File

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

37
main/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "view",
"private": true,
"version": "0.3.2-dl",
"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"
}

37
main/pages/auth/code.vue Normal file
View File

@ -0,0 +1,37 @@
<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>

52
main/pages/library.vue Normal file
View File

@ -0,0 +1,52 @@
<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,52 +1,165 @@
<template> <template>
<div <div
class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden" class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
> >
<!-- banner image --> <div class="absolute inset-0 z-0">
<div class="absolute flex top-0 h-fit inset-x-0 z-[-20]"> <img
<img :src="bannerUrl" class="w-full h-auto object-cover" /> :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 <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" class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
> >
{{ game.mName }} {{ game.mName }}
</h1> </h1>
<div
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900" <div class="flex flex-row gap-x-4 items-stretch mb-8">
/> <!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
</div>
<!-- main page -->
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<!-- game toolbar -->
<div class="h-full flex flex-row gap-x-4 items-stretch">
<GameStatusButton <GameStatusButton
@install="() => installFlow()" @install="() => installFlow()"
@launch="() => launch()" @launch="() => launch()"
@queue="() => queue()" @queue="() => queue()"
@uninstall="() => uninstall()" @uninstall="() => uninstall()"
@kill="() => kill()" @kill="() => kill()"
@options="() => (configureModalOpen = true)"
@resume="() => resumeDownload()"
:status="status" :status="status"
/> />
<a <a
:href="remoteUrl" :href="remoteUrl"
target="_blank" target="_blank"
type="button" 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" 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" /> <BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store Store
</a> </a>
</div> </div>
</div> </div>
<!-- Main content -->
<div class="w-full bg-zinc-900 px-8 py-6">
<div class="grid grid-cols-[2fr,1fr] gap-8">
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<div
v-html="htmlDescription"
class="prose prose-invert prose-blue overflow-y-auto custom-scrollbar max-w-none"
></div>
</div>
</div>
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
Game Images
</h2>
<div class="relative">
<div v-if="mediaUrls.length > 0">
<div
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
>
<div
class="absolute inset-0"
@click="fullscreenImage = mediaUrls[currentImageIndex]"
>
<TransitionGroup name="slide" tag="div" class="h-full">
<img
v-for="(url, index) in mediaUrls"
:key="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> </div>
<ModalTemplate v-model="installFlowOpen"> <ModalTemplate v-model="installFlowOpen">
<template #default> <template #default>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left"> <div class="mt-3 text-center sm:mt-0 sm:text-left">
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100" <h3 class="text-base font-semibold text-zinc-100">
>Install {{ game.mName }}? Install {{ game.mName }}?
</DialogTitle> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-zinc-400"> <p class="text-sm text-zinc-400">
Drop will add {{ game.mName }} to the queue to be downloaded. Drop will add {{ game.mName }} to the queue to be downloaded.
@ -237,9 +350,7 @@
<template #buttons> <template #buttons>
<LoadingButton <LoadingButton
@click="() => install()" @click="() => install()"
:disabled=" :disabled="!(versionOptions && versionOptions.length > 0)"
!(versionOptions && versionOptions.length > 0 && !installDir)
"
:loading="installLoading" :loading="installLoading"
type="submit" type="submit"
class="ml-2 w-full sm:w-fit" class="ml-2 w-full sm:w-fit"
@ -256,14 +367,89 @@
</button> </button>
</template> </template>
</ModalTemplate> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
Dialog,
DialogTitle,
TransitionChild,
TransitionRoot,
Listbox, Listbox,
ListboxButton, ListboxButton,
ListboxLabel, ListboxLabel,
@ -274,10 +460,17 @@ import {
CheckIcon, CheckIcon,
ChevronUpDownIcon, ChevronUpDownIcon,
WrenchIcon, WrenchIcon,
ChevronLeftIcon,
ChevronRightIcon,
XMarkIcon,
ArrowsPointingOutIcon,
PhotoIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline"; import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid"; import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { micromark } from "micromark";
import { GameStatusEnum } from "~/types";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -290,13 +483,27 @@ const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`, path: `/store/${game.value.id}`,
}); });
const bannerUrl = await useObject(game.value.mBannerId); 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 installFlowOpen = ref(false);
const versionOptions = ref< const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }> undefined | Array<{ versionName: string; platform: string }>
>(); >();
const installDirs = ref<undefined | Array<string>>(); const installDirs = ref<undefined | Array<string>>();
const currentImageIndex = ref(0);
const configureModalOpen = ref(false);
async function installFlow() { async function installFlow() {
installFlowOpen.value = true; installFlowOpen.value = true;
versionOptions.value = undefined; versionOptions.value = undefined;
@ -319,8 +526,7 @@ const installVersionIndex = ref(0);
const installDir = ref(0); const installDir = ref(0);
async function install() { async function install() {
try { try {
if (!versionOptions.value) if (!versionOptions.value) throw new Error("Versions have not been loaded");
throw new Error("Versions have not been loaded");
installLoading.value = true; installLoading.value = true;
await invoke("download_game", { await invoke("download_game", {
gameId: game.value.id, gameId: game.value.id,
@ -335,6 +541,14 @@ async function install() {
installLoading.value = false; installLoading.value = false;
} }
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id });
} catch (e) {
console.error(e);
}
}
async function launch() { async function launch() {
try { try {
await invoke("launch_game", { id: game.value.id }); await invoke("launch_game", { id: game.value.id });
@ -376,4 +590,61 @@ async function kill() {
console.error(e); console.error(e);
} }
} }
function nextImage() {
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
}
function previousImage() {
currentImageIndex.value =
(currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
}
const fullscreenImage = ref<string | null>(null);
</script> </script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.slide-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgb(82 82 91) transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgb(82 82 91);
border-radius: 3px;
}
</style>

View File

@ -0,0 +1,19 @@
<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

@ -91,7 +91,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types"; import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => { window.addEventListener("resize", (event) => {
@ -158,7 +163,7 @@ function loadGamesForQueue(v: typeof queue.value) {
if (games.value[id]) return; if (games.value[id]) return;
(async () => { (async () => {
const gameData = await useGame(id); const gameData = await useGame(id);
const cover = await useObject(gameData.game.mCoverId); const cover = await useObject(gameData.game.mCoverObjectId);
games.value[id] = { ...gameData, cover }; games.value[id] = { ...gameData, cover };
})(); })();
} }
@ -167,7 +172,7 @@ function loadGamesForQueue(v: typeof queue.value) {
loadGamesForQueue(queue.value); loadGamesForQueue(queue.value);
async function onEnd(event: { oldIndex: number; newIndex: number }) { async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_game_in_queue", { await invoke("move_download_in_queue", {
oldIndex: event.oldIndex, oldIndex: event.oldIndex,
newIndex: event.newIndex, newIndex: event.newIndex,
}); });

View File

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

View File

@ -0,0 +1,64 @@
<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

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

View File

@ -1,10 +1,15 @@
<template> <template>
<div> <div class="border-b border-zinc-700 py-5">
<div class="border-b border-zinc-600 py-2 px-1"> <h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
<div Downloads
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap" </h3>
> </div>
<div class="ml-4 mt-2">
<div class="mt-5">
<div class="border-b border-zinc-600">
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div class="ml-4 mt-2 pb-4">
<h3 class="text-base font-display font-semibold text-zinc-100"> <h3 class="text-base font-display font-semibold text-zinc-100">
Install directories Install directories
</h3> </h3>
@ -15,27 +20,17 @@
</p> </p>
</div> </div>
<div class="ml-4 mt-2 shrink-0"> <div class="ml-4 mt-2 shrink-0">
<button <button @click="() => (open = true)" type="button"
@click="() => (open = true)" class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
type="button"
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Add new directory Add new directory
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<ul role="list" class="divide-y divide-gray-800"> <ul role="list" class="divide-y divide-gray-800">
<li <li v-for="(dir, dirIdx) in dirs" :key="dir" class="flex justify-between gap-x-6 py-5">
v-for="(dir, dirIdx) in dirs"
:key="dir"
class="flex justify-between gap-x-6 py-5"
>
<div class="flex min-w-0 gap-x-4"> <div class="flex min-w-0 gap-x-4">
<FolderIcon <FolderIcon class="h-6 w-6 text-blue-600 flex-none rounded-full" alt="" />
class="h-6 w-6 text-blue-600 flex-none rounded-full"
alt=""
/>
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="text-sm/6 text-zinc-100"> <p class="text-sm/6 text-zinc-100">
{{ dir }} {{ dir }}
@ -43,16 +38,12 @@
</div> </div>
</div> </div>
<div class="flex shrink-0 items-center gap-x-6"> <div class="flex shrink-0 items-center gap-x-6">
<button <button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[
@click="() => deleteDirectory(dirIdx)"
:disabled="dirs.length <= 1"
:class="[
dirs.length <= 1 dirs.length <= 1
? 'text-zinc-700' ? 'text-zinc-700'
: 'text-zinc-400 hover:text-zinc-100', : 'text-zinc-400 hover:text-zinc-100',
'-m-2.5 block p-2.5', '-m-2.5 block p-2.5',
]" ]">
>
<span class="sr-only">Open options</span> <span class="sr-only">Open options</span>
<TrashIcon class="size-5" aria-hidden="true" /> <TrashIcon class="size-5" aria-hidden="true" />
</button> </button>
@ -72,78 +63,72 @@
Maximum Download Threads Maximum Download Threads
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads"
type="number" @keypress="validateNumberInput" @paste="validatePaste"
name="threads" class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" />
id="threads"
min="1"
max="32"
v-model="downloadThreads"
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div> </div>
<p class="mt-2 text-sm text-zinc-400"> <p class="mt-2 text-sm text-zinc-400">
The maximum number of concurrent download threads. Higher values may The maximum number of concurrent download threads. Higher values may
download faster but use more system resources. Default is 4. download faster but use more system resources. Default is 4.
</p> </p>
</div> </div>
<div class="mt-10 space-y-8">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Force Offline</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Drop will not make any external connections
</p>
</div>
<Switch v-model="forceOffline" :class="[
forceOffline ? 'bg-blue-600' : 'bg-zinc-700',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
]">
<span :class="[
forceOffline ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
]" />
</Switch>
</div>
</div>
<div class="mt-6"> <div class="mt-6">
<button <button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
type="button" 'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
@click="saveDownloadThreads" saveState.success
class="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 disabled:bg-blue-600/50 disabled:cursor-not-allowed" ? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
> : 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
Save Changes 'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
]">
{{ saveState.success ? 'Saved' : 'Save Changes' }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<TransitionRoot as="template" :show="open"> <TransitionRoot as="template" :show="open">
<Dialog class="relative z-50" @close="open = false"> <Dialog class="relative z-50" @close="open = false">
<TransitionChild <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
as="template" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
enter="ease-out duration-300" <div class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity" />
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity"
/>
</TransitionChild> </TransitionChild>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto"> <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0" <TransitionChild as="template" enter="ease-out duration-300"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
>
<DialogPanel <DialogPanel
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6" class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mt-3 w-full sm:ml-4 sm:mt-0"> <div class="mt-3 w-full sm:ml-4 sm:mt-0">
<div> <div>
<label <label for="dir" class="block text-sm/6 font-medium text-zinc-100">Select game directory</label>
for="dir"
class="block text-sm/6 font-medium text-zinc-100"
>Select game directory</label
>
<div class="mt-2"> <div class="mt-2">
<button <button @click="() => selectDirectory()"
@click="() => selectDirectory()" class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6">
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6"
>
{{ {{
currentDirectory ?? "Click to select a directory..." currentDirectory ?? "Click to select a directory..."
}} }}
@ -156,36 +141,25 @@
</div> </div>
</div> </div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<LoadingButton <LoadingButton :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading"
:disabled="currentDirectory == undefined" @click="() => submitDirectory()" :class="[
type="button"
:loading="createDirectoryLoading"
@click="() => submitDirectory()"
:class="[
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto', 'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
currentDirectory === undefined currentDirectory === undefined
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10' ? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
: 'text-white bg-blue-600 hover:bg-blue-500', : 'text-white bg-blue-600 hover:bg-blue-500',
]" ]">
>
Add Add
</LoadingButton> </LoadingButton>
<button <button type="button"
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto" class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="() => cancelDirectory()" @click="() => cancelDirectory()" ref="cancelButtonRef">
ref="cancelButtonRef"
>
Cancel Cancel
</button> </button>
</div> </div>
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4"> <div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<XCircleIcon <XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
class="h-5 w-5 text-red-600"
aria-hidden="true"
/>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-red-600"> <h3 class="text-sm font-medium text-red-600">
@ -211,6 +185,7 @@ import {
} from "@headlessui/vue"; } from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid"; import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { Switch } from '@headlessui/vue'
import { type Settings } from "~/types"; import { type Settings } from "~/types";
const open = ref(false); const open = ref(false);
@ -222,6 +197,12 @@ const dirs = ref<Array<string>>([]);
const settings = await invoke<Settings>("fetch_settings"); const settings = await invoke<Settings>("fetch_settings");
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4); const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
const forceOffline = ref(settings?.forceOffline ?? false);
const saveState = reactive({
loading: false,
success: false
});
async function updateDirs() { async function updateDirs() {
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats"); const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
@ -279,10 +260,41 @@ async function deleteDirectory(index: number) {
await updateDirs(); await updateDirs();
} }
async function saveDownloadThreads() { async function saveSettings() {
//Would save download threads downloadThreads.value); try {
saveState.loading = true;
await invoke("update_settings", { await invoke("update_settings", {
newSettings: { maxDownloadThreads: downloadThreads.value }, newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value },
}); });
// Show success state
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 {
saveState.loading = false;
}
}
function validateNumberInput(event: KeyboardEvent) {
// Allow only numbers and basic control keys
if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault();
}
}
function validatePaste(event: ClipboardEvent) {
// Prevent paste if content contains non-numeric characters
const pastedData = event.clipboardData?.getData('text');
if (pastedData && !/^\d+$/.test(pastedData)) {
event.preventDefault();
}
} }
</script> </script>

View File

@ -0,0 +1,59 @@
<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

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

5
main/tsconfig.json Normal file
View File

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

View File

@ -17,7 +17,7 @@ export type User = {
username: string; username: string;
admin: boolean; admin: boolean;
displayName: string; displayName: string;
profilePicture: string; profilePictureObjectId: string;
}; };
export type AppState = { export type AppState = {
@ -30,14 +30,20 @@ export type Game = {
mName: string; mName: string;
mShortDescription: string; mShortDescription: string;
mDescription: string; mDescription: string;
mIconId: string; mIconObjectId: string;
mBannerId: string; mBannerObjectId: string;
mCoverId: string; mCoverObjectId: string;
mImageLibrary: string[]; mImageLibraryObjectIds: string[];
mImageCarouselObjectIds: string[];
};
export type GameVersion = {
launchCommandTemplate: string;
}; };
export enum AppStatus { export enum AppStatus {
NotConfigured = "NotConfigured", NotConfigured = "NotConfigured",
Offline = "Offline",
SignedOut = "SignedOut", SignedOut = "SignedOut",
SignedIn = "SignedIn", SignedIn = "SignedIn",
SignedInNeedsReauth = "SignedInNeedsReauth", SignedInNeedsReauth = "SignedInNeedsReauth",
@ -48,32 +54,36 @@ export enum GameStatusEnum {
Remote = "Remote", Remote = "Remote",
Queued = "Queued", Queued = "Queued",
Downloading = "Downloading", Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed", Installed = "Installed",
Updating = "Updating", Updating = "Updating",
Uninstalling = "Uninstalling", Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired", SetupRequired = "SetupRequired",
Running = "Running" Running = "Running",
PartiallyInstalled = "PartiallyInstalled",
} }
export type GameStatus = { export type GameStatus = {
type: GameStatusEnum; type: GameStatusEnum;
version_name?: string; version_name?: string;
install_dir?: string;
}; };
export enum DownloadableType { export enum DownloadableType {
Game = "Game", Game = "Game",
Tool = "Tool", Tool = "Tool",
DLC = "DLC", DLC = "DLC",
Mod = "Mod" Mod = "Mod",
} }
export type DownloadableMetadata = { export type DownloadableMetadata = {
id: string, id: string;
version: string, version: string;
downloadType: DownloadableType downloadType: DownloadableType;
} };
export type Settings = { export type Settings = {
autostart: boolean, autostart: boolean;
maxDownloadThreads: number, maxDownloadThreads: number;
} forceOffline: boolean;
};

8091
main/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

22
optimize-appimage.sh Executable file
View File

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

View File

@ -1,72 +0,0 @@
<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>

View File

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

View File

@ -1,66 +0,0 @@
<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 />
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
const rawGames: Array<Game> = await invoke("fetch_library");
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

@ -1,3 +0,0 @@
<template>
</template>

View File

@ -1,60 +0,0 @@
<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

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

3659
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

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