mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-14 00:31:33 +10:00
Compare commits
213 Commits
v0.2.0-bet
...
346ee1dddc
| Author | SHA1 | Date | |
|---|---|---|---|
| 346ee1dddc | |||
| 3b830e2a44 | |||
| 75a4b73ee1 | |||
| 339d707092 | |||
| 776dc8fe7a | |||
| dbe8c8df4d | |||
| 35f49b8811 | |||
| cc5339a389 | |||
| 6104bfda72 | |||
| be688cb18f | |||
| 13cc69f10e | |||
| 574782f445 | |||
| b5a8543194 | |||
| d0e4aea5ce | |||
| 739e6166c5 | |||
| 682c6e9c0b | |||
| 46e1f16cdd | |||
| d19f9bbc31 | |||
| 2913fdf35b | |||
| f9fdf151ea | |||
| 495d93705e | |||
| c477dd4872 | |||
| f560a62c8f | |||
| 2874b9776b | |||
| afcd4e916f | |||
| 885fa42ecc | |||
| 6d295bd47f | |||
| c3ee09af85 | |||
| 0ce55e12c5 | |||
| 86bce1c68d | |||
| 924e4e334c | |||
| 065eb2356a | |||
| 689e9ad890 | |||
| 7c35ed73aa | |||
| 8f261a5dac | |||
| 67b6f2aa2e | |||
| 11e2b3fe8a | |||
| eba224f998 | |||
| d045385a5d | |||
| d878806ade | |||
| b71081006e | |||
| c9e1ed78eb | |||
| 446aa70b0b | |||
| 1d0b81078a | |||
| 5251a56c3c | |||
| eeca8a7a98 | |||
| 365cdaf311 | |||
| 2957773179 | |||
| 15e5fe4dc0 | |||
| 2dc0a78354 | |||
| 51c480f245 | |||
| 95d223e2b2 | |||
| 790e8c2afe | |||
| 02edb2cbc1 | |||
| 4b4c0734ec | |||
| e75e0044fb | |||
| 65561abdab | |||
| fed3e08dce | |||
| b0b1e397b1 | |||
| 7b443818d1 | |||
| fa4a881cc0 | |||
| 4f16a6e6b2 | |||
| 47d9e9949b | |||
| a643d6102b | |||
| a71ff160c2 | |||
| a53a566792 | |||
| ac6b034501 | |||
| 5ef20f7a57 | |||
| 8e5e3b2715 | |||
| 0f717d51d0 | |||
| 4941f2a6fa | |||
| 40eb19cf8b | |||
| 6b9b9e3606 | |||
| 3e074abc0a | |||
| 1fdf569278 | |||
| 77251a6524 | |||
| 137b71b3ba | |||
| 569ba4243c | |||
| 834f52d024 | |||
| 1ce6be80db | |||
| 19c8fc24aa | |||
| 4239215451 | |||
| 9614af7f03 | |||
| 639d3b4630 | |||
| cdcd69391d | |||
| 8520b255a3 | |||
| d9c4f7aa75 | |||
| 316a3742eb | |||
| b9df197534 | |||
| 5c479cb283 | |||
| 4c59c5d6c1 | |||
| 9977107374 | |||
| 2690c3019d | |||
| 2a1a7326d0 | |||
| f33ca95bdf | |||
| bb23e88ead | |||
| 810fbdfe49 | |||
| e204ff30b4 | |||
| 501145c5d9 | |||
| dca5f65e89 | |||
| 00f55ff3ae | |||
| 52c70052a4 | |||
| 7a0cf4fbb6 | |||
| 76bae3d926 | |||
| 53234d283e | |||
| 3e10f1749a | |||
| 6d7630e7c0 | |||
| b6a54c0d09 | |||
| 9897698322 | |||
| 4ef49cc832 | |||
| 6ad383799d | |||
| e0ea8c9a57 | |||
| 4fc0855ba1 | |||
| f50818697f | |||
| 39f2ebd2d6 | |||
| 89ea34c94e | |||
| 92729701c3 | |||
| 7d4651db69 | |||
| 5db9ae5f98 | |||
| 4d8eadc491 | |||
| 3ca87fc45b | |||
| 21204dee69 | |||
| cfc9d13cad | |||
| 5bb04dafdd | |||
| 23077040ce | |||
| b99ff67e69 | |||
| f183a9d1a2 | |||
| fc6bab9381 | |||
| 170fde5e23 | |||
| c2f54c1dbc | |||
| d83aae6dc4 | |||
| 9a184a8f35 | |||
| fd30b3e402 | |||
| cf19477d4d | |||
| 5f5cbd07c6 | |||
| 0381b8b8cb | |||
| 9369ff14b8 | |||
| 7c3140e424 | |||
| 9e29aa7a76 | |||
| 604d5b5884 | |||
| 245a84d20b | |||
| f1c8bbf8dd | |||
| 60d0a48a1a | |||
| 7ab53f3357 | |||
| 4e93eb440c | |||
| 94cf6788d8 | |||
| 3eda9799c5 | |||
| f29e989aff | |||
| 182361e598 | |||
| 50f37fd022 | |||
| 5ea47d733b | |||
| 2822b7a593 | |||
| 82804ebc67 | |||
| 8aad64ffa7 | |||
| 9e82a0b3c3 | |||
| 6ea4cf2797 | |||
| 165a9671fd | |||
| 25ba200a5e | |||
| 32ae7d5385 | |||
| 005bab2fb8 | |||
| 06d1e9ed95 | |||
| a56ee25581 | |||
| dceaa56ade | |||
| 6159319172 | |||
| 8be1dd435c | |||
| cac612b176 | |||
| 6568faaf4f | |||
| ea70ec9453 | |||
| f64782e5d4 | |||
| a2e63aa2c8 | |||
| 78149bbb3c | |||
| a846eed306 | |||
| 1a89135342 | |||
| 0a2ac25b1c | |||
| aed58e49bc | |||
| 881fcc6abe | |||
| b4d70a35b3 | |||
| b6c64e56e5 | |||
| 3299c71b3d | |||
| 2c8164e54f | |||
| 02f8591a60 | |||
| 0a0d9d6294 | |||
| a17311a88d | |||
| 472eb1d435 | |||
| 6b96e408b2 | |||
| c3f62222fe | |||
| 01e6162527 | |||
| 88b2505e71 | |||
| 95f2174f8d | |||
| 7c90d2b8fd | |||
| d7b0302bdd | |||
| 3ccd44466f | |||
| 93b8b83c20 | |||
| 1861659daa | |||
| 327628b780 | |||
| f4ac1c87cd | |||
| 03fa3646fa | |||
| a881d8e248 | |||
| dcb2c0f004 | |||
| c722a54132 | |||
| e72662c4a8 | |||
| 139bc0ca36 | |||
| 949acfc161 | |||
| 9af0d08875 | |||
| dcb1564568 | |||
| 1f899ec349 | |||
| 6a8d0af87d | |||
| 21835858f1 | |||
| a135b1321c | |||
| ad92dbec08 | |||
| 85a08990c3 | |||
| dd7f5675d8 | |||
| 9ea2aa4997 |
23
.github/workflows/clippy.yml
vendored
Normal file
23
.github/workflows/clippy.yml
vendored
Normal 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
104
.github/workflows/release.yml
vendored
Normal 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
3
.gitignore
vendored
@ -27,3 +27,6 @@ dist-ssr
|
|||||||
|
|
||||||
src-tauri/flamegraph.svg
|
src-tauri/flamegraph.svg
|
||||||
src-tauri/perf*
|
src-tauri/perf*
|
||||||
|
|
||||||
|
/*.AppImage
|
||||||
|
/squashfs-root
|
||||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal 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
|
||||||
@ -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
48
build.mjs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import type { AppState } from "~/types";
|
|
||||||
|
|
||||||
export const useAppState = () => useState<AppState>("state");
|
|
||||||
@ -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
1
libs/drop-base
Submodule
Submodule libs/drop-base added at 04125e89be
@ -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();
|
||||||
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 6.5 MiB |
31
main/components/GameOptions/Launch.vue
Normal file
31
main/components/GameOptions/Launch.vue
Normal 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>
|
||||||
122
main/components/GameOptionsModal.vue
Normal file
122
main/components/GameOptionsModal.vue
Normal 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>
|
||||||
183
main/components/GameStatusButton.vue
Normal file
183
main/components/GameStatusButton.vue
Normal 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>
|
||||||
@ -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> = [
|
||||||
{
|
{
|
||||||
5
main/components/HeaderButton.vue
Normal file
5
main/components/HeaderButton.vue
Normal 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>
|
||||||
@ -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,27 +83,22 @@ 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",
|
||||||
});
|
});
|
||||||
|
|
||||||
function navigate(close: () => any, to: NavigationItem){
|
function navigate(close: () => any, to: NavigationItem) {
|
||||||
close();
|
close();
|
||||||
router.push(to.route);
|
router.push(to.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigation: NavigationItem[] = [
|
const navigation: NavigationItem[] = [
|
||||||
{
|
|
||||||
label: "Account settings",
|
|
||||||
route: "/account",
|
|
||||||
prefix: "",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "App settings",
|
label: "App settings",
|
||||||
route: "/settings",
|
route: "/settings",
|
||||||
@ -110,6 +108,6 @@ const navigation: NavigationItem[] = [
|
|||||||
label: "Quit Drop",
|
label: "Quit Drop",
|
||||||
route: "/quit",
|
route: "/quit",
|
||||||
prefix: "",
|
prefix: "",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
@ -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">→</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 →
|
||||||
|
</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() {
|
||||||
196
main/components/LibrarySearch.vue
Normal file
196
main/components/LibrarySearch.vue
Normal 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>
|
||||||
7
main/components/LoadingIndicator.vue
Normal file
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const loading = useLoadingIndicator();
|
||||||
|
|
||||||
|
watch(loading.isLoading, console.log);
|
||||||
|
</script>
|
||||||
@ -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" />
|
||||||
17
main/components/OfflineHeaderWidget.vue
Normal file
17
main/components/OfflineHeaderWidget.vue
Normal 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>
|
||||||
@ -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";
|
||||||
|
|
||||||
async function close(){
|
const window = getCurrentWindow();
|
||||||
console.log(window);
|
const showMinimise = await window.isMinimizable();
|
||||||
const result = await window.close();
|
|
||||||
console.log(`closed window: ${result}`);
|
async function close() {
|
||||||
|
await window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const window = getCurrentWindow();
|
async function minimise() {
|
||||||
|
await window.minimize();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
3
main/composables/app-state.ts
Normal file
3
main/composables/app-state.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { AppState } from "~/types";
|
||||||
|
|
||||||
|
export const useAppState = () => useState<AppState | undefined>("state");
|
||||||
@ -26,5 +26,7 @@ export const useCurrentNavigationIndex = (
|
|||||||
currentNavigation.value = calculateCurrentNavIndex(to);
|
currentNavigation.value = calculateCurrentNavIndex(to);
|
||||||
});
|
});
|
||||||
|
|
||||||
return currentNavigation;
|
return {currentNavigation, recalculateNavigation: () => {
|
||||||
|
currentNavigation.value = calculateCurrentNavIndex(route);
|
||||||
|
}};
|
||||||
};
|
};
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
@ -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
91
main/error.vue
Normal 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">←</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
82
main/layouts/default.vue
Normal 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>
|
||||||
@ -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 />
|
||||||
@ -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
37
main/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "view",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.3.1",
|
||||||
|
"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
37
main/pages/auth/code.vue
Normal 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">←</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
52
main/pages/library.vue
Normal 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>
|
||||||
@ -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>
|
||||||
19
main/pages/library/index.vue
Normal file
19
main/pages/library/index.vue
Normal 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>
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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>
|
||||||
64
main/pages/settings/account.vue
Normal file
64
main/pages/settings/account.vue
Normal 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>
|
||||||
@ -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;
|
||||||
@ -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,37 +63,44 @@
|
|||||||
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"
|
|
||||||
@keypress="validateNumberInput"
|
|
||||||
@paste="validatePaste"
|
|
||||||
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-zinc-400">
|
<p class="mt-2 text-sm text-zinc-400">
|
||||||
The maximum number of concurrent download threads. Higher values may
|
The maximum number of concurrent download threads. Higher values may
|
||||||
download faster but use more system resources. Default is 4.
|
download faster but use more system resources. Default is 4.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-10 space-y-8">
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium leading-6 text-zinc-100">Force Offline</h3>
|
||||||
|
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||||
|
Drop will not make any external connections
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="forceOffline" :class="[
|
||||||
|
forceOffline ? 'bg-blue-600' : 'bg-zinc-700',
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
|
||||||
|
]">
|
||||||
|
<span :class="[
|
||||||
|
forceOffline ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
|
||||||
|
]" />
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<button
|
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
|
||||||
type="button"
|
|
||||||
@click="saveDownloadThreads"
|
|
||||||
:disabled="saveState.loading"
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
||||||
saveState.success
|
saveState.success
|
||||||
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||||
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
||||||
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
||||||
]"
|
]">
|
||||||
>
|
|
||||||
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -110,49 +108,27 @@
|
|||||||
</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..."
|
||||||
}}
|
}}
|
||||||
@ -165,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">
|
||||||
@ -220,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);
|
||||||
@ -231,6 +197,7 @@ const dirs = ref<Array<string>>([]);
|
|||||||
|
|
||||||
const settings = await invoke<Settings>("fetch_settings");
|
const settings = await invoke<Settings>("fetch_settings");
|
||||||
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
||||||
|
const forceOffline = ref(settings?.forceOffline ?? false);
|
||||||
|
|
||||||
const saveState = reactive({
|
const saveState = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -293,11 +260,11 @@ async function deleteDirectory(index: number) {
|
|||||||
await updateDirs();
|
await updateDirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDownloadThreads() {
|
async function saveSettings() {
|
||||||
try {
|
try {
|
||||||
saveState.loading = true;
|
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
|
// Show success state
|
||||||
59
main/pages/settings/index.vue
Normal file
59
main/pages/settings/index.vue
Normal 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>
|
||||||
37
main/pages/store/index.vue
Normal file
37
main/pages/store/index.vue
Normal 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>
|
||||||
@ -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
5
main/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"exclude": ["src-tauri/**/*"]
|
||||||
|
}
|
||||||
@ -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
8091
main/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
22
optimize-appimage.sh
Executable file
22
optimize-appimage.sh
Executable 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"
|
||||||
41
package.json
41
package.json
@ -1,41 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0-beta",
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1,78 +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 :libraryDownloadError = "libraryDownloadError" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
|
|
||||||
|
|
||||||
let libraryDownloadError = false;
|
|
||||||
|
|
||||||
async function calculateGames(): Promise<Game[]> {
|
|
||||||
try {
|
|
||||||
return await invoke("fetch_library");
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
libraryDownloadError = true;
|
|
||||||
return new Array();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawGames: Array<Game> = await calculateGames();
|
|
||||||
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
|
|
||||||
const icons = await Promise.all(
|
|
||||||
games.map(({ game, status }) => useObject(game.mIconId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigation = games.map(({ game, status }) => {
|
|
||||||
const isInstalled = computed(
|
|
||||||
() =>
|
|
||||||
status.value.type == GameStatusEnum.Installed ||
|
|
||||||
status.value.type == GameStatusEnum.SetupRequired
|
|
||||||
);
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
label: game.mName,
|
|
||||||
route: `/library/${game.id}`,
|
|
||||||
prefix: `/library/${game.id}`,
|
|
||||||
isInstalled,
|
|
||||||
};
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
|
|
||||||
</script>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ libraryDownloadError: boolean }>();
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
|
|
||||||
Library Failed to update
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
@ -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>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<template></template>
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
3659
src-tauri/Cargo.lock
generated
3659
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.2.0-beta-prerelease-1"
|
version = "0.3.1"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -16,8 +16,6 @@ tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
|||||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
name = "drop_app_lib"
|
name = "drop_app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build]
|
|
||||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||||
|
|
||||||
|
|
||||||
@ -25,11 +23,9 @@ rustflags = ["-C", "target-feature=+aes,+sse2"]
|
|||||||
tauri-build = { version = "2.0.0", features = [] }
|
tauri-build = { version = "2.0.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri-plugin-shell = "2.0.0"
|
tauri-plugin-shell = "2.2.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde-binary = "0.5.0"
|
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
directories = "5.0.1"
|
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
@ -50,11 +46,42 @@ slice-deque = "0.3.0"
|
|||||||
throttle_my_fn = "0.2.6"
|
throttle_my_fn = "0.2.6"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
atomic-instant-full = "0.1.0"
|
atomic-instant-full = "0.1.0"
|
||||||
|
cacache = "13.1.0"
|
||||||
|
http-serde = "2.1.1"
|
||||||
|
reqwest-middleware = "0.4.0"
|
||||||
|
reqwest-middleware-cache = "0.1.1"
|
||||||
|
deranged = "=0.4.0"
|
||||||
|
droplet-rs = "0.7.3"
|
||||||
|
gethostname = "1.0.1"
|
||||||
|
zstd = "0.13.3"
|
||||||
|
tar = "0.4.44"
|
||||||
|
rand = "0.9.1"
|
||||||
|
regex = "1.11.1"
|
||||||
|
tempfile = "3.19.1"
|
||||||
|
schemars = "0.8.22"
|
||||||
|
sha1 = "0.10.6"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
whoami = "1.6.0"
|
||||||
|
filetime = "0.2.25"
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
known-folders = "1.2.0"
|
||||||
|
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||||
|
tauri-plugin-opener = "2.4.0"
|
||||||
|
bitcode = "0.6.6"
|
||||||
|
reqwest-websocket = "0.5.0"
|
||||||
|
futures-lite = "2.6.0"
|
||||||
|
page_size = "0.6.0"
|
||||||
|
sysinfo = "0.36.1"
|
||||||
|
humansize = "2.1.3"
|
||||||
|
# tailscale = { path = "./tailscale" }
|
||||||
|
|
||||||
|
[dependencies.dynfmt]
|
||||||
|
version = "0.1.5"
|
||||||
|
features = ["curly"]
|
||||||
|
|
||||||
[dependencies.tauri]
|
[dependencies.tauri]
|
||||||
version = "2.1.1"
|
version = "2.7.0"
|
||||||
features = ["tray-icon"]
|
features = ["protocol-asset", "tray-icon"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.40.0"
|
version = "1.40.0"
|
||||||
@ -70,23 +97,16 @@ features = ["fs"]
|
|||||||
|
|
||||||
[dependencies.uuid]
|
[dependencies.uuid]
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
features = [
|
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||||
"v4", # Lets you generate random UUIDs
|
|
||||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
|
||||||
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies.openssl]
|
|
||||||
version = "0.10.66"
|
|
||||||
features = ["vendored"]
|
|
||||||
|
|
||||||
[dependencies.rustbreak]
|
[dependencies.rustbreak]
|
||||||
version = "2"
|
version = "2"
|
||||||
features = [] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12"
|
version = "0.12"
|
||||||
features = ["json", "blocking"]
|
default-features = false
|
||||||
|
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"os:default"
|
"os:default",
|
||||||
|
"opener:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
@ -17,7 +17,6 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
|
|||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.settings.autostart = enabled;
|
db_handle.settings.autostart = enabled;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
save_db();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user