mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 00:02:41 +10:00
Compare commits
213 Commits
v0.2.0-bet
...
v0.3.2-dl
| Author | SHA1 | Date | |
|---|---|---|---|
| 124d51bced | |||
| 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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -26,4 +26,7 @@ dist-ssr
|
||||
.output
|
||||
|
||||
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
|
||||
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
|
||||
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>
|
||||
<LoadingIndicator />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
@ -9,8 +10,6 @@
|
||||
import "~/composables/downloads.js";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AppStatus } from "~/types";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useAppState } from "./composables/app-state.js";
|
||||
import {
|
||||
initialNavigation,
|
||||
@ -20,18 +19,26 @@ import {
|
||||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
|
||||
router.beforeEach(async () => {
|
||||
async function fetchState() {
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
if (!state.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `App state is: ${state.value}`,
|
||||
fatal: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await fetchState();
|
||||
|
||||
// This is inefficient but apparently we do it lol
|
||||
router.beforeEach(async () => {
|
||||
await fetchState();
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
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"
|
||||
:class="[
|
||||
'transition uppercase font-display font-semibold text-md',
|
||||
navIdx === currentPageIndex
|
||||
navIdx === currentNavigation
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 hover:text-zinc-200',
|
||||
]"
|
||||
@ -28,9 +28,7 @@
|
||||
/>
|
||||
<div class="inline-flex items-center">
|
||||
<ol class="inline-flex gap-3">
|
||||
<HeaderQueueWidget
|
||||
:object="currentQueueObject"
|
||||
/>
|
||||
<HeaderQueueWidget :object="currentQueueObject" />
|
||||
<li v-for="(item, itemIdx) in quickActions">
|
||||
<HeaderWidget
|
||||
@click="item.action"
|
||||
@ -39,21 +37,23 @@
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
</li>
|
||||
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
||||
<HeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<WindowControl class="h-16 w-16 p-4" />
|
||||
<WindowControl />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const window = getCurrentWindow();
|
||||
const state = useAppState();
|
||||
|
||||
const navigation: Array<NavigationItem> = [
|
||||
{
|
||||
@ -78,7 +78,7 @@ const navigation: Array<NavigationItem> = [
|
||||
},
|
||||
];
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
const { currentNavigation } = useCurrentNavigationIndex(navigation);
|
||||
|
||||
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>
|
||||
<Menu v-if="state.user" as="div" class="relative inline-block">
|
||||
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<HeaderWidget>
|
||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||
@ -23,7 +23,7 @@
|
||||
<MenuItems
|
||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||
>
|
||||
<PanelWidget class="flex-col gap-y-2">
|
||||
<div class="flex-col gap-y-2">
|
||||
<NuxtLink
|
||||
to="/id/me"
|
||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||
@ -49,20 +49,23 @@
|
||||
Admin Dashboard
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem v-for="(nav, navIdx) in navigation" v-slot="{ active, close }">
|
||||
<MenuItem
|
||||
v-for="(nav, navIdx) in navigation"
|
||||
v-slot="{ active, close }"
|
||||
>
|
||||
<button
|
||||
@click="() => navigate(close, nav)"
|
||||
@click="() => navigate(close, nav)"
|
||||
:href="nav.route"
|
||||
:class="[
|
||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
|
||||
'transition text-left block px-4 py-2 text-sm',
|
||||
]"
|
||||
>
|
||||
{{ nav.label }}</button
|
||||
>
|
||||
{{ nav.label }}
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</PanelWidget>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
@ -80,27 +83,22 @@ const open = ref(false);
|
||||
const router = useRouter();
|
||||
router.afterEach(() => {
|
||||
open.value = false;
|
||||
})
|
||||
});
|
||||
|
||||
const state = useAppState();
|
||||
const profilePictureUrl: string = await invoke("gen_drop_url", {
|
||||
path: `/api/v1/object/${state.value.user?.profilePicture}`,
|
||||
});
|
||||
const profilePictureUrl: string = await useObject(
|
||||
state.value?.user?.profilePictureObjectId ?? ""
|
||||
);
|
||||
const adminUrl: string = await invoke("gen_drop_url", {
|
||||
path: "/admin",
|
||||
});
|
||||
|
||||
function navigate(close: () => any, to: NavigationItem){
|
||||
function navigate(close: () => any, to: NavigationItem) {
|
||||
close();
|
||||
router.push(to.route);
|
||||
}
|
||||
|
||||
const navigation: NavigationItem[] = [
|
||||
{
|
||||
label: "Account settings",
|
||||
route: "/account",
|
||||
prefix: "",
|
||||
},
|
||||
{
|
||||
label: "App settings",
|
||||
route: "/settings",
|
||||
@ -110,6 +108,6 @@ const navigation: NavigationItem[] = [
|
||||
label: "Quit Drop",
|
||||
route: "/quit",
|
||||
prefix: "",
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@ -13,11 +13,7 @@
|
||||
<div class="max-w-lg">
|
||||
<slot />
|
||||
<div class="mt-10">
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
||||
>
|
||||
<div>
|
||||
<div v-if="loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@ -37,10 +33,19 @@
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span v-else>
|
||||
Sign in with your browser <span aria-hidden="true">→</span>
|
||||
<span class="inline-flex gap-x-8 items-center" v-else>
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
|
||||
>
|
||||
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</button>
|
||||
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
||||
Use a code →
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
@ -121,11 +126,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
let offerManualTimeout: NodeJS.Timeout | undefined;
|
||||
const offerManual = ref(false);
|
||||
const manualToken = ref("");
|
||||
const manualLoading = ref(false);
|
||||
@ -135,14 +142,16 @@ async function auth() {
|
||||
}
|
||||
|
||||
function authWrapper_wrapper() {
|
||||
error.value = undefined;
|
||||
loading.value = true;
|
||||
auth().catch((e) => {
|
||||
loading.value = false;
|
||||
error.value = e;
|
||||
if (offerManualTimeout) clearTimeout(offerManualTimeout);
|
||||
});
|
||||
setTimeout(() => {
|
||||
offerManualTimeout = setTimeout(() => {
|
||||
offerManual.value = true;
|
||||
}, 10000);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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>
|
||||
<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()">
|
||||
<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>
|
||||
<HeaderButton v-if="showMinimise" @click="() => minimise()">
|
||||
<MinusIcon />
|
||||
</HeaderButton>
|
||||
<HeaderButton @click="() => close()">
|
||||
<XMarkIcon />
|
||||
</HeaderButton>
|
||||
@ -8,11 +11,14 @@
|
||||
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
async function close(){
|
||||
console.log(window);
|
||||
const result = await window.close();
|
||||
console.log(`closed window: ${result}`);
|
||||
const window = getCurrentWindow();
|
||||
const showMinimise = await window.isMinimizable();
|
||||
|
||||
async function close() {
|
||||
await window.close();
|
||||
}
|
||||
|
||||
const window = getCurrentWindow();
|
||||
async function minimise() {
|
||||
await window.minimize();
|
||||
}
|
||||
</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);
|
||||
});
|
||||
|
||||
return currentNavigation;
|
||||
return {currentNavigation, recalculateNavigation: () => {
|
||||
currentNavigation.value = calculateCurrentNavIndex(route);
|
||||
}};
|
||||
};
|
||||
@ -1,8 +1,9 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
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> } = {};
|
||||
|
||||
@ -13,7 +14,6 @@ export type SerializedGameStatus = [
|
||||
];
|
||||
|
||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
console.log(status);
|
||||
if (status[0]) {
|
||||
return {
|
||||
type: status[0].type,
|
||||
@ -31,27 +31,43 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
|
||||
export const useGame = async (gameId: string) => {
|
||||
if (!gameRegistry[gameId]) {
|
||||
const data: { game: Game; status: SerializedGameStatus } = await invoke(
|
||||
"fetch_game",
|
||||
{
|
||||
gameId,
|
||||
}
|
||||
);
|
||||
gameRegistry[gameId] = data.game;
|
||||
const data: {
|
||||
game: Game;
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
} = await invoke("fetch_game", {
|
||||
gameId,
|
||||
});
|
||||
gameRegistry[gameId] = { game: data.game, version: data.version };
|
||||
if (!gameStatusRegistry[gameId]) {
|
||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||
|
||||
listen(`update_game/${gameId}`, (event) => {
|
||||
const payload: {
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
} = event.payload as any;
|
||||
console.log(payload.status);
|
||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||
|
||||
/**
|
||||
* I am not super happy about this.
|
||||
*
|
||||
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
|
||||
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
|
||||
* on transient state updates.
|
||||
*/
|
||||
if (payload.version) {
|
||||
gameRegistry[gameId].version = payload.version;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const game = gameRegistry[gameId];
|
||||
const status = gameStatusRegistry[gameId];
|
||||
return { game, status };
|
||||
};
|
||||
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 { data } from "autoprefixer";
|
||||
import { AppStatus, type AppState } from "~/types";
|
||||
|
||||
export function setupHooks() {
|
||||
const router = useRouter();
|
||||
const state = useAppState();
|
||||
|
||||
listen("auth/processing", (event) => {
|
||||
router.push("/auth/processing");
|
||||
@ -15,8 +17,9 @@ export function setupHooks() {
|
||||
);
|
||||
});
|
||||
|
||||
listen("auth/finished", (event) => {
|
||||
router.push("/store");
|
||||
listen("auth/finished", async (event) => {
|
||||
router.push("/library");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
});
|
||||
|
||||
listen("download_error", (event) => {
|
||||
@ -27,12 +30,31 @@ export function setupHooks() {
|
||||
description: `Drop encountered an error while downloading your game: "${(
|
||||
event.payload as unknown as string
|
||||
).toString()}"`,
|
||||
buttonText: "Close"
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
});
|
||||
|
||||
// This is for errors that (we think) aren't our fault
|
||||
listen("launch_external_error", (event) => {
|
||||
createModal(
|
||||
ModalType.Confirmation,
|
||||
{
|
||||
title: "Did something go wrong?",
|
||||
description:
|
||||
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
|
||||
buttonText: "Open",
|
||||
},
|
||||
async (e, c) => {
|
||||
if (e == "confirm") {
|
||||
await invoke("open_process_logs", { gameId: event.payload });
|
||||
}
|
||||
c();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
@ -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();
|
||||
|
||||
switch (state.value.status) {
|
||||
@ -60,6 +88,6 @@ export function initialNavigation(state: Ref<AppState>) {
|
||||
router.push("/error/serverunavailable");
|
||||
break;
|
||||
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>
|
||||
<div class="flex flex-col bg-zinc-950 overflow-hidden">
|
||||
<div class="flex flex-col bg-zinc-950 overflow-hidden h-screen">
|
||||
<MiniHeader />
|
||||
<div class="relative grow overflow-y-auto">
|
||||
<slot />
|
||||
@ -13,5 +13,9 @@ export default defineNuxtConfig({
|
||||
|
||||
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.2-dl",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt generate",
|
||||
"dev": "nuxt dev",
|
||||
"postinstall": "nuxt prepare",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"koa": "^2.16.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.16.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
37
main/pages/auth/code.vue
Normal file
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,41 +1,154 @@
|
||||
<template>
|
||||
<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 flex top-0 h-fit inset-x-0 z-[-20]">
|
||||
<img :src="bannerUrl" class="w-full h-auto object-cover" />
|
||||
<h1
|
||||
class="absolute inset-x-0 w-fit mx-auto text-center top-32 -translate-y-[50%] text-4xl text-zinc-100 font-bold font-display z-50 p-4 shadow-xl bg-zinc-900/80 rounded-xl"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img
|
||||
:src="bannerUrl"
|
||||
class="w-full h-[24rem] object-cover blur-sm scale-105"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
|
||||
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>
|
||||
<!-- main page -->
|
||||
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
|
||||
<!-- game toolbar -->
|
||||
<div class="h-full flex flex-row gap-x-4 items-stretch">
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
:href="remoteUrl"
|
||||
target="_blank"
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-zinc-800/50 px-4 font-semibold text-white shadow-sm hover:bg-zinc-800/80 uppercase font-display"
|
||||
>
|
||||
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
|
||||
|
||||
Store
|
||||
</a>
|
||||
<div class="relative z-10">
|
||||
<div class="px-8 pb-4">
|
||||
<h1
|
||||
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
@options="() => (configureModalOpen = true)"
|
||||
@resume="() => resumeDownload()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
:href="remoteUrl"
|
||||
target="_blank"
|
||||
type="button"
|
||||
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display"
|
||||
>
|
||||
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
|
||||
Store
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="w-full bg-zinc-900 px-8 py-6">
|
||||
<div class="grid grid-cols-[2fr,1fr] gap-8">
|
||||
<div class="space-y-6">
|
||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||
<div
|
||||
v-html="htmlDescription"
|
||||
class="prose prose-invert prose-blue overflow-y-auto custom-scrollbar max-w-none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
||||
Game Images
|
||||
</h2>
|
||||
<div class="relative">
|
||||
<div v-if="mediaUrls.length > 0">
|
||||
<div
|
||||
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@click="fullscreenImage = mediaUrls[currentImageIndex]"
|
||||
>
|
||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
v-show="index === currentImageIndex"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-between px-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
>
|
||||
<div class="pointer-events-auto">
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="previousImage()"
|
||||
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronLeftIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pointer-events-auto">
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="nextImage()"
|
||||
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRightIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-4 right-4 flex items-center gap-x-2 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
>
|
||||
<ArrowsPointingOutIcon class="size-5" />
|
||||
<span class="text-sm font-medium">View Fullscreen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute -bottom-2 left-1/2 -translate-x-1/2 flex gap-x-2"
|
||||
>
|
||||
<button
|
||||
v-for="(_, index) in mediaUrls"
|
||||
:key="index"
|
||||
@click.stop="currentImageIndex = index"
|
||||
class="w-1.5 h-1.5 rounded-full transition-all"
|
||||
:class="[
|
||||
currentImageIndex === index
|
||||
? 'bg-zinc-100 scale-125'
|
||||
: 'bg-zinc-600 hover:bg-zinc-500',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="aspect-video rounded-lg overflow-hidden bg-zinc-900/50 flex flex-col items-center justify-center text-center px-4"
|
||||
>
|
||||
<PhotoIcon class="size-12 text-zinc-500 mb-2" />
|
||||
<p class="text-zinc-400 font-medium">No images available</p>
|
||||
<p class="text-zinc-500 text-sm">
|
||||
Game screenshots will appear here when available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,9 +157,9 @@
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
</DialogTitle>
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Install {{ game.mName }}?
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Drop will add {{ game.mName }} to the queue to be downloaded.
|
||||
@ -237,9 +350,7 @@
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="
|
||||
!(versionOptions && versionOptions.length > 0 && !installDir)
|
||||
"
|
||||
:disabled="!(versionOptions && versionOptions.length > 0)"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
@ -256,14 +367,89 @@
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
|
||||
<!--
|
||||
Dear future DecDuck,
|
||||
This v-if is necessary for Vue rendering reasons
|
||||
(it tries to access the game version for not installed games)
|
||||
You have already tried to remove it
|
||||
Don't.
|
||||
-->
|
||||
<GameOptionsModal
|
||||
v-if="status.type === GameStatusEnum.Installed"
|
||||
v-model="configureModalOpen"
|
||||
:game-id="game.id"
|
||||
/>
|
||||
|
||||
<Transition
|
||||
enter="transition ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="transition ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="fullscreenImage"
|
||||
class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
|
||||
@click="fullscreenImage = null"
|
||||
>
|
||||
<div
|
||||
class="relative w-full h-full flex items-center justify-center"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
class="absolute top-4 right-4 p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
@click.stop="fullscreenImage = null"
|
||||
>
|
||||
<XMarkIcon class="size-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="previousImage()"
|
||||
class="absolute left-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
>
|
||||
<ChevronLeftIcon class="size-6" />
|
||||
</button>
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="nextImage()"
|
||||
class="absolute right-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
>
|
||||
<ChevronRightIcon class="size-6" />
|
||||
</button>
|
||||
|
||||
<TransitionGroup
|
||||
name="slide"
|
||||
tag="div"
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
@click.stop
|
||||
>
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
v-show="currentImageIndex === index"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
|
||||
<div
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-zinc-900/50 backdrop-blur-sm"
|
||||
>
|
||||
<p class="text-zinc-100 text-sm font-medium">
|
||||
{{ currentImageIndex + 1 }} / {{ mediaUrls.length }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
@ -274,10 +460,17 @@ import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
WrenchIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
XMarkIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
PhotoIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { micromark } from "micromark";
|
||||
import { GameStatusEnum } from "~/types";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -290,13 +483,27 @@ const remoteUrl: string = await invoke("gen_drop_url", {
|
||||
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 versionOptions = ref<
|
||||
undefined | Array<{ versionName: string; platform: string }>
|
||||
>();
|
||||
const installDirs = ref<undefined | Array<string>>();
|
||||
const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
@ -319,8 +526,7 @@ const installVersionIndex = ref(0);
|
||||
const installDir = ref(0);
|
||||
async function install() {
|
||||
try {
|
||||
if (!versionOptions.value)
|
||||
throw new Error("Versions have not been loaded");
|
||||
if (!versionOptions.value) throw new Error("Versions have not been loaded");
|
||||
installLoading.value = true;
|
||||
await invoke("download_game", {
|
||||
gameId: game.value.id,
|
||||
@ -335,6 +541,14 @@ async function install() {
|
||||
installLoading.value = false;
|
||||
}
|
||||
|
||||
async function resumeDownload() {
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { id: game.value.id });
|
||||
@ -376,4 +590,61 @@ async function kill() {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function nextImage() {
|
||||
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
|
||||
}
|
||||
|
||||
function previousImage() {
|
||||
currentImageIndex.value =
|
||||
(currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
|
||||
}
|
||||
|
||||
const fullscreenImage = ref<string | null>(null);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(82 82 91) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(82 82 91);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
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">
|
||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
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);
|
||||
window.addEventListener("resize", (event) => {
|
||||
@ -158,7 +163,7 @@ function loadGamesForQueue(v: typeof queue.value) {
|
||||
if (games.value[id]) return;
|
||||
(async () => {
|
||||
const gameData = await useGame(id);
|
||||
const cover = await useObject(gameData.game.mCoverId);
|
||||
const cover = await useObject(gameData.game.mCoverObjectId);
|
||||
games.value[id] = { ...gameData, cover };
|
||||
})();
|
||||
}
|
||||
@ -167,7 +172,7 @@ function loadGamesForQueue(v: typeof queue.value) {
|
||||
loadGamesForQueue(queue.value);
|
||||
|
||||
async function onEnd(event: { oldIndex: number; newIndex: number }) {
|
||||
await invoke("move_game_in_queue", {
|
||||
await invoke("move_download_in_queue", {
|
||||
oldIndex: event.oldIndex,
|
||||
newIndex: event.newIndex,
|
||||
});
|
||||
@ -10,13 +10,13 @@
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<li v-for="(item, itemIdx) in navigation" :key="item.prefix">
|
||||
<NuxtLink :href="item.route" :class="[
|
||||
itemIdx === currentPageIndex
|
||||
itemIdx === currentNavigation
|
||||
? 'bg-zinc-800/50 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
|
||||
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
|
||||
]">
|
||||
<component :is="item.icon" :class="[
|
||||
itemIdx === currentPageIndex
|
||||
itemIdx === currentNavigation
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 group-hover:text-zinc-200',
|
||||
'transition h-6 w-6 shrink-0',
|
||||
@ -45,6 +45,7 @@ import type { Component } from "vue";
|
||||
import type { NavigationItem } from "~/types";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { UserIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const systemData = await invoke<{
|
||||
clientId: string;
|
||||
@ -101,6 +102,12 @@ const navigation = computed(() => [
|
||||
prefix: "/settings/downloads",
|
||||
icon: ArrowDownTrayIcon,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
route: "/settings/account",
|
||||
prefix: "/settings/account",
|
||||
icon: UserIcon
|
||||
},
|
||||
...(isDebugMode.value ? [{
|
||||
label: "Debug Info",
|
||||
route: "/settings/debug",
|
||||
@ -112,10 +119,10 @@ const navigation = computed(() => [
|
||||
const currentPlatform = platform();
|
||||
|
||||
// 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(navigation, (newNav) => {
|
||||
currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
|
||||
currentNavigation.value = useCurrentNavigationIndex(newNav).currentNavigation.value;
|
||||
});
|
||||
</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;
|
||||
}>("fetch_system_data");
|
||||
|
||||
console.log(systemData);
|
||||
|
||||
clientId.value = systemData.clientId;
|
||||
baseUrl.value = systemData.baseUrl;
|
||||
dataDir.value = systemData.dataDir;
|
||||
@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="border-b border-zinc-600 py-2 px-1">
|
||||
<div
|
||||
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
|
||||
>
|
||||
<div class="ml-4 mt-2">
|
||||
<div class="border-b border-zinc-700 py-5">
|
||||
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||
Downloads
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="border-b border-zinc-600">
|
||||
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
||||
<div class="ml-4 mt-2 pb-4">
|
||||
<h3 class="text-base font-display font-semibold text-zinc-100">
|
||||
Install directories
|
||||
</h3>
|
||||
@ -15,27 +20,17 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 mt-2 shrink-0">
|
||||
<button
|
||||
@click="() => (open = true)"
|
||||
type="button"
|
||||
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<button @click="() => (open = true)" type="button"
|
||||
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
|
||||
Add new directory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul role="list" class="divide-y divide-gray-800">
|
||||
<li
|
||||
v-for="(dir, dirIdx) in dirs"
|
||||
:key="dir"
|
||||
class="flex justify-between gap-x-6 py-5"
|
||||
>
|
||||
<li v-for="(dir, dirIdx) in dirs" :key="dir" class="flex justify-between gap-x-6 py-5">
|
||||
<div class="flex min-w-0 gap-x-4">
|
||||
<FolderIcon
|
||||
class="h-6 w-6 text-blue-600 flex-none rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
<FolderIcon class="h-6 w-6 text-blue-600 flex-none rounded-full" alt="" />
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="text-sm/6 text-zinc-100">
|
||||
{{ dir }}
|
||||
@ -43,16 +38,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-x-6">
|
||||
<button
|
||||
@click="() => deleteDirectory(dirIdx)"
|
||||
:disabled="dirs.length <= 1"
|
||||
:class="[
|
||||
dirs.length <= 1
|
||||
? 'text-zinc-700'
|
||||
: 'text-zinc-400 hover:text-zinc-100',
|
||||
'-m-2.5 block p-2.5',
|
||||
]"
|
||||
>
|
||||
<button @click="() => deleteDirectory(dirIdx)" :disabled="dirs.length <= 1" :class="[
|
||||
dirs.length <= 1
|
||||
? 'text-zinc-700'
|
||||
: 'text-zinc-400 hover:text-zinc-100',
|
||||
'-m-2.5 block p-2.5',
|
||||
]">
|
||||
<span class="sr-only">Open options</span>
|
||||
<TrashIcon class="size-5" aria-hidden="true" />
|
||||
</button>
|
||||
@ -72,37 +63,44 @@
|
||||
Maximum Download Threads
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="number"
|
||||
name="threads"
|
||||
id="threads"
|
||||
min="1"
|
||||
max="32"
|
||||
v-model="downloadThreads"
|
||||
@keypress="validateNumberInput"
|
||||
@paste="validatePaste"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<input type="number" name="threads" id="threads" min="1" max="32" v-model="downloadThreads"
|
||||
@keypress="validateNumberInput" @paste="validatePaste"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
The maximum number of concurrent download threads. Higher values may
|
||||
download faster but use more system resources. Default is 4.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-10 space-y-8">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">Force Offline</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Drop will not make any external connections
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model="forceOffline" :class="[
|
||||
forceOffline ? 'bg-blue-600' : 'bg-zinc-700',
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
|
||||
]">
|
||||
<span :class="[
|
||||
forceOffline ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
|
||||
]" />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
@click="saveDownloadThreads"
|
||||
:disabled="saveState.loading"
|
||||
:class="[
|
||||
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
||||
saveState.success
|
||||
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
||||
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
<button type="button" @click="saveSettings" :disabled="saveState.loading" :class="[
|
||||
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
|
||||
saveState.success
|
||||
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
|
||||
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
|
||||
]">
|
||||
{{ saveState.success ? 'Saved' : 'Save Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
@ -110,49 +108,27 @@
|
||||
</div>
|
||||
<TransitionRoot as="template" :show="open">
|
||||
<Dialog class="relative z-50" @close="open = false">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity"
|
||||
/>
|
||||
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
||||
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||
<div class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<TransitionChild as="template" enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<DialogPanel
|
||||
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
|
||||
>
|
||||
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 w-full sm:ml-4 sm:mt-0">
|
||||
<div>
|
||||
<label
|
||||
for="dir"
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Select game directory</label
|
||||
>
|
||||
<label for="dir" class="block text-sm/6 font-medium text-zinc-100">Select game directory</label>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
@click="() => selectDirectory()"
|
||||
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<button @click="() => selectDirectory()"
|
||||
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6">
|
||||
{{
|
||||
currentDirectory ?? "Click to select a directory..."
|
||||
}}
|
||||
@ -165,36 +141,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<LoadingButton
|
||||
:disabled="currentDirectory == undefined"
|
||||
type="button"
|
||||
:loading="createDirectoryLoading"
|
||||
@click="() => submitDirectory()"
|
||||
:class="[
|
||||
<LoadingButton :disabled="currentDirectory == undefined" type="button" :loading="createDirectoryLoading"
|
||||
@click="() => submitDirectory()" :class="[
|
||||
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
|
||||
currentDirectory === undefined
|
||||
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
|
||||
: 'text-white bg-blue-600 hover:bg-blue-500',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
Add
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
<button type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="() => cancelDirectory()"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
@click="() => cancelDirectory()" ref="cancelButtonRef">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon
|
||||
class="h-5 w-5 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
@ -220,6 +185,7 @@ import {
|
||||
} from "@headlessui/vue";
|
||||
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import { type Settings } from "~/types";
|
||||
|
||||
const open = ref(false);
|
||||
@ -231,6 +197,7 @@ const dirs = ref<Array<string>>([]);
|
||||
|
||||
const settings = await invoke<Settings>("fetch_settings");
|
||||
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
|
||||
const forceOffline = ref(settings?.forceOffline ?? false);
|
||||
|
||||
const saveState = reactive({
|
||||
loading: false,
|
||||
@ -293,21 +260,21 @@ async function deleteDirectory(index: number) {
|
||||
await updateDirs();
|
||||
}
|
||||
|
||||
async function saveDownloadThreads() {
|
||||
async function saveSettings() {
|
||||
try {
|
||||
saveState.loading = true;
|
||||
await invoke("update_settings", {
|
||||
newSettings: { maxDownloadThreads: downloadThreads.value },
|
||||
newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value },
|
||||
});
|
||||
|
||||
|
||||
// Show success state
|
||||
saveState.success = true;
|
||||
|
||||
|
||||
// Reset back to normal state after 2 seconds
|
||||
setTimeout(() => {
|
||||
saveState.success = false;
|
||||
}, 2000);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
} finally {
|
||||
@ -317,8 +284,8 @@ async function saveDownloadThreads() {
|
||||
|
||||
function validateNumberInput(event: KeyboardEvent) {
|
||||
// Allow only numbers and basic control keys
|
||||
if (!/^\d$/.test(event.key) &&
|
||||
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
if (!/^\d$/.test(event.key) &&
|
||||
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
// Also possible
|
||||
/*
|
||||
nuxtApp.hook("vue:error", (error, instance, info) => {
|
||||
|
||||
console.error(error, info);
|
||||
const router = useRouter();
|
||||
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;
|
||||
admin: boolean;
|
||||
displayName: string;
|
||||
profilePicture: string;
|
||||
profilePictureObjectId: string;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
@ -30,14 +30,20 @@ export type Game = {
|
||||
mName: string;
|
||||
mShortDescription: string;
|
||||
mDescription: string;
|
||||
mIconId: string;
|
||||
mBannerId: string;
|
||||
mCoverId: string;
|
||||
mImageLibrary: string[];
|
||||
mIconObjectId: string;
|
||||
mBannerObjectId: string;
|
||||
mCoverObjectId: string;
|
||||
mImageLibraryObjectIds: string[];
|
||||
mImageCarouselObjectIds: string[];
|
||||
};
|
||||
|
||||
export type GameVersion = {
|
||||
launchCommandTemplate: string;
|
||||
};
|
||||
|
||||
export enum AppStatus {
|
||||
NotConfigured = "NotConfigured",
|
||||
Offline = "Offline",
|
||||
SignedOut = "SignedOut",
|
||||
SignedIn = "SignedIn",
|
||||
SignedInNeedsReauth = "SignedInNeedsReauth",
|
||||
@ -48,32 +54,36 @@ export enum GameStatusEnum {
|
||||
Remote = "Remote",
|
||||
Queued = "Queued",
|
||||
Downloading = "Downloading",
|
||||
Validating = "Validating",
|
||||
Installed = "Installed",
|
||||
Updating = "Updating",
|
||||
Uninstalling = "Uninstalling",
|
||||
SetupRequired = "SetupRequired",
|
||||
Running = "Running"
|
||||
Running = "Running",
|
||||
PartiallyInstalled = "PartiallyInstalled",
|
||||
}
|
||||
|
||||
export type GameStatus = {
|
||||
type: GameStatusEnum;
|
||||
version_name?: string;
|
||||
install_dir?: string;
|
||||
};
|
||||
|
||||
export enum DownloadableType {
|
||||
Game = "Game",
|
||||
Tool = "Tool",
|
||||
DLC = "DLC",
|
||||
Mod = "Mod"
|
||||
Mod = "Mod",
|
||||
}
|
||||
|
||||
export type DownloadableMetadata = {
|
||||
id: string,
|
||||
version: string,
|
||||
downloadType: DownloadableType
|
||||
}
|
||||
id: string;
|
||||
version: string;
|
||||
downloadType: DownloadableType;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
autostart: boolean,
|
||||
maxDownloadThreads: number,
|
||||
}
|
||||
autostart: boolean;
|
||||
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",
|
||||
"private": true,
|
||||
"version": "0.2.0-beta",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"build": "node ./build.mjs",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"nuxt": "^3.13.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tauri-apps/cli": ">=2.0.0",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
"@tauri-apps/cli": "^2.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
name = "drop-app"
|
||||
version = "0.2.0-beta-prerelease-1"
|
||||
version = "0.3.2-dl"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# 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
|
||||
name = "drop_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build]
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
|
||||
|
||||
@ -25,11 +23,9 @@ rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde_json = "1"
|
||||
serde-binary = "0.5.0"
|
||||
rayon = "1.10.0"
|
||||
directories = "5.0.1"
|
||||
webbrowser = "1.0.2"
|
||||
url = "2.5.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
@ -50,11 +46,42 @@ slice-deque = "0.3.0"
|
||||
throttle_my_fn = "0.2.6"
|
||||
parking_lot = "0.12.3"
|
||||
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]
|
||||
version = "2.1.1"
|
||||
features = ["tray-icon"]
|
||||
|
||||
version = "2.7.0"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.40.0"
|
||||
@ -70,23 +97,16 @@ features = ["fs"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.10.0"
|
||||
features = [
|
||||
"v4", # Lets you generate random UUIDs
|
||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
||||
]
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.10.66"
|
||||
features = ["vendored"]
|
||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||
|
||||
[dependencies.rustbreak]
|
||||
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]
|
||||
version = "0.12"
|
||||
features = ["json", "blocking"]
|
||||
default-features = false
|
||||
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"core:window:allow-close",
|
||||
"deep-link: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 tauri::AppHandle;
|
||||
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();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user