mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-12 15:52:43 +10:00
Compare commits
220 Commits
v0.2.0-bet
...
52-feature
| Author | SHA1 | Date | |
|---|---|---|---|
| 96df57ac54 | |||
| 8069616f2b | |||
| 5eef2bf60f | |||
| ec6294b8e7 | |||
| 17c375bcab | |||
| cb55ac2bf5 | |||
| e11db851a5 | |||
| 16365713cf | |||
| 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 xdg-utils
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-latest'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -t 3600 -u build.keychain
|
||||
|
||||
curl https://droposs.org/drop.crt --output drop.pem
|
||||
sudo security authorizationdb write com.apple.trust-settings.user allow
|
||||
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
|
||||
sudo security authorizationdb remove com.apple.trust-settings.user
|
||||
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported. Using identity: $CERT_ID"
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
NO_STRIP: true
|
||||
with:
|
||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'Auto-release v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: ${{ matrix.args }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,3 +27,6 @@ dist-ssr
|
||||
|
||||
src-tauri/flamegraph.svg
|
||||
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() {
|
||||
198
main/components/LibrarySearch.vue
Normal file
198
main/components/LibrarySearch.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input type="text" v-model="searchQuery"
|
||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
placeholder="Search library..." />
|
||||
</div>
|
||||
<button @click="() => calculateGames(true)"
|
||||
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, navIndex) 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',
|
||||
navIndex === 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 v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
|
||||
<div role="status">
|
||||
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
|
||||
fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { 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-zinc-400",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||
};
|
||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Not installed",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading...",
|
||||
[GameStatusEnum.Validating]: "Validating...",
|
||||
[GameStatusEnum.Installed]: "Installed",
|
||||
[GameStatusEnum.Updating]: "Updating...",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||
[GameStatusEnum.Running]: "Running",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
const loading = ref(false);
|
||||
const games: {
|
||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||
} = {};
|
||||
const icons: { [key: string]: string } = {};
|
||||
|
||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) {
|
||||
rawGames.value = [];
|
||||
loading.value = true;
|
||||
}
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<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);
|
||||
}
|
||||
loading.value = false;
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
// Wait up to 300 ms for the library to load, otherwise
|
||||
// show the loading state while we while
|
||||
await new Promise<void>((r) => {
|
||||
let hasResolved = false;
|
||||
const resolveFunc = () => {
|
||||
if (!hasResolved) r();
|
||||
hasResolved = true
|
||||
|
||||
}
|
||||
calculateGames(true).then(resolveFunc);
|
||||
setTimeout(resolveFunc, 300);
|
||||
})
|
||||
|
||||
const navigation = computed(() =>
|
||||
rawGames.value.map((game) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type != GameStatusEnum.Remote
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
const route = useRoute();
|
||||
const currentNavigation = computed(() => {
|
||||
return navigation.value.findIndex((e) => e.route == route.path)
|
||||
});
|
||||
|
||||
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();
|
||||
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
||||
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,44 @@ 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) => {
|
||||
console.log(event);
|
||||
const payload: {
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
} = event.payload as any;
|
||||
console.log(payload.status);
|
||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||
|
||||
/**
|
||||
* I am not super happy about this.
|
||||
*
|
||||
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
|
||||
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
|
||||
* on transient state updates.
|
||||
*/
|
||||
if (payload.version) {
|
||||
gameRegistry[gameId].version = payload.version;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const game = gameRegistry[gameId];
|
||||
const status = gameStatusRegistry[gameId];
|
||||
return { game, status };
|
||||
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",
|
||||
"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>
|
||||
16
main/pages/community/index.vue
Normal file
16
main/pages/community/index.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
||||
<div>
|
||||
<Wordmark />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
||||
Under construction
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
||||
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
||||
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
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>
|
||||
674
main/pages/library/[id]/index.vue
Normal file
674
main/pages/library/[id]/index.vue
Normal file
@ -0,0 +1,674 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
|
||||
>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img
|
||||
:src="bannerUrl"
|
||||
class="w-full h-[24rem] object-cover blur-sm scale-105"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-zinc-900/80 to-transparent opacity-90"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-zinc-900/95 via-zinc-900/80 to-transparent opacity-90"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="px-8 pb-4">
|
||||
<h1
|
||||
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
@options="() => (configureModalOpen = true)"
|
||||
@resume="() => resumeDownload()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
:href="remoteUrl"
|
||||
target="_blank"
|
||||
type="button"
|
||||
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display"
|
||||
>
|
||||
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
|
||||
Store
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="w-full bg-zinc-900 px-8 py-6">
|
||||
<div class="grid grid-cols-[2fr,1fr] gap-8">
|
||||
<div class="space-y-6">
|
||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||
<div
|
||||
v-html="htmlDescription"
|
||||
class="prose prose-invert prose-blue overflow-y-auto custom-scrollbar max-w-none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
||||
Game Images
|
||||
</h2>
|
||||
<div class="relative">
|
||||
<div v-if="mediaUrls.length > 0">
|
||||
<div
|
||||
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@click="fullscreenImage = mediaUrls[currentImageIndex]"
|
||||
>
|
||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
v-show="index === currentImageIndex"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-between px-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
>
|
||||
<div class="pointer-events-auto">
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="previousImage()"
|
||||
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronLeftIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pointer-events-auto">
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="nextImage()"
|
||||
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRightIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-4 right-4 flex items-center gap-x-2 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
>
|
||||
<ArrowsPointingOutIcon class="size-5" />
|
||||
<span class="text-sm font-medium">View Fullscreen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute -bottom-2 left-1/2 -translate-x-1/2 flex gap-x-2"
|
||||
>
|
||||
<button
|
||||
v-for="(_, index) in mediaUrls"
|
||||
:key="index"
|
||||
@click.stop="currentImageIndex = index"
|
||||
class="w-1.5 h-1.5 rounded-full transition-all"
|
||||
:class="[
|
||||
currentImageIndex === index
|
||||
? 'bg-zinc-100 scale-125'
|
||||
: 'bg-zinc-600 hover:bg-zinc-500',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="aspect-video rounded-lg overflow-hidden bg-zinc-900/50 flex flex-col items-center justify-center text-center px-4"
|
||||
>
|
||||
<PhotoIcon class="size-12 text-zinc-500 mb-2" />
|
||||
<p class="text-zinc-400 font-medium">No images available</p>
|
||||
<p class="text-zinc-500 text-sm">
|
||||
Game screenshots will appear here when available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalTemplate v-model="installFlowOpen">
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Install {{ game.mName }}?
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Drop will add {{ game.mName }} to the queue to be downloaded.
|
||||
While downloading, Drop may use up a large amount of resources,
|
||||
particularly network bandwidth and CPU utilisation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<div v-if="versionOptions && versionOptions.length > 0">
|
||||
<Listbox as="div" v-model="installVersionIndex">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Version</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate"
|
||||
>{{ versionOptions[installVersionIndex].versionName }}
|
||||
on
|
||||
{{ versionOptions[installVersionIndex].platform }}</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(version, versionIdx) in versionOptions"
|
||||
:key="version.versionName"
|
||||
:value="versionIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version.versionName }} on
|
||||
{{ version.platform }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="versionOptions === null || versionOptions?.length == 0"
|
||||
class="mt-1 rounded-md bg-red-600/10 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
There are no supported versions to install. Please contact your
|
||||
server admin or try again later.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full flex items-center justify-center p-4">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-7 h-7 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Install to</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate">{{
|
||||
installDirs[installDir]
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(dir, dirIdx) in installDirs"
|
||||
:key="dir"
|
||||
:value="dirIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ dir }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="text-zinc-400 text-sm mt-2">
|
||||
Add more install directories in
|
||||
<PageWidget to="/settings/downloads">
|
||||
<WrenchIcon class="size-3" />
|
||||
Settings
|
||||
</PageWidget>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ installError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="!(versionOptions && versionOptions.length > 0)"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="installFlowOpen = false"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
|
||||
<!--
|
||||
Dear future DecDuck,
|
||||
This v-if is necessary for Vue rendering reasons
|
||||
(it tries to access the game version for not installed games)
|
||||
You have already tried to remove it
|
||||
Don't.
|
||||
-->
|
||||
<GameOptionsModal
|
||||
v-if="status.type === GameStatusEnum.Installed"
|
||||
v-model="configureModalOpen"
|
||||
:game-id="game.id"
|
||||
/>
|
||||
|
||||
<Transition
|
||||
enter="transition ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="transition ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="fullscreenImage"
|
||||
class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
|
||||
@click="fullscreenImage = null"
|
||||
>
|
||||
<div
|
||||
class="relative w-full h-full flex items-center justify-center"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
class="absolute top-4 right-4 p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
@click.stop="fullscreenImage = null"
|
||||
>
|
||||
<XMarkIcon class="size-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="previousImage()"
|
||||
class="absolute left-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
>
|
||||
<ChevronLeftIcon class="size-6" />
|
||||
</button>
|
||||
<button
|
||||
v-if="mediaUrls.length > 1"
|
||||
@click.stop="nextImage()"
|
||||
class="absolute right-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
|
||||
>
|
||||
<ChevronRightIcon class="size-6" />
|
||||
</button>
|
||||
|
||||
<TransitionGroup
|
||||
name="slide"
|
||||
tag="div"
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
@click.stop
|
||||
>
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
v-show="currentImageIndex === index"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
|
||||
<div
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-zinc-900/50 backdrop-blur-sm"
|
||||
>
|
||||
<p class="text-zinc-100 text-sm font-medium">
|
||||
{{ currentImageIndex + 1 }} / {{ mediaUrls.length }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
WrenchIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
XMarkIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
PhotoIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { micromark } from "micromark";
|
||||
import { GameStatusEnum } from "~/types";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const id = route.params.id.toString();
|
||||
|
||||
const { game: rawGame, status } = await useGame(id);
|
||||
const game = ref(rawGame);
|
||||
|
||||
const remoteUrl: string = await invoke("gen_drop_url", {
|
||||
path: `/store/${game.value.id}`,
|
||||
});
|
||||
|
||||
const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||
|
||||
// Get all available images
|
||||
const mediaUrls = await Promise.all(
|
||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||
const src = await useObject(v);
|
||||
return src;
|
||||
})
|
||||
);
|
||||
|
||||
const htmlDescription = micromark(game.value.mDescription);
|
||||
|
||||
const installFlowOpen = ref(false);
|
||||
const versionOptions = ref<
|
||||
undefined | Array<{ versionName: string; platform: string }>
|
||||
>();
|
||||
const installDirs = ref<undefined | Array<string>>();
|
||||
const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
installDirs.value = undefined;
|
||||
|
||||
try {
|
||||
versionOptions.value = await invoke("fetch_game_version_options", {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
versionOptions.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const installLoading = ref(false);
|
||||
const installError = ref<string | undefined>();
|
||||
const installVersionIndex = ref(0);
|
||||
const installDir = ref(0);
|
||||
async function install() {
|
||||
try {
|
||||
if (!versionOptions.value) throw new Error("Versions have not been loaded");
|
||||
installLoading.value = true;
|
||||
await invoke("download_game", {
|
||||
gameId: game.value.id,
|
||||
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
|
||||
installDir: installDir.value,
|
||||
});
|
||||
installFlowOpen.value = false;
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
}
|
||||
|
||||
installLoading.value = false;
|
||||
}
|
||||
|
||||
async function resumeDownload() {
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { id: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: `Couldn't run "${game.value.mName}"`,
|
||||
description: `Drop failed to launch "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function queue() {
|
||||
router.push("/queue");
|
||||
}
|
||||
|
||||
async function uninstall() {
|
||||
await invoke("uninstall_game", { gameId: game.value.id });
|
||||
}
|
||||
|
||||
async function kill() {
|
||||
try {
|
||||
await invoke("kill_game", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: `Couldn't stop "${game.value.mName}"`,
|
||||
description: `Drop failed to stop "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function nextImage() {
|
||||
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
|
||||
}
|
||||
|
||||
function previousImage() {
|
||||
currentImageIndex.value =
|
||||
(currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
|
||||
}
|
||||
|
||||
const fullscreenImage = ref<string | null>(null);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(82 82 91) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(82 82 91);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
16
main/pages/news/index.vue
Normal file
16
main/pages/news/index.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
||||
<div>
|
||||
<Wordmark />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
||||
Under construction
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
||||
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
||||
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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,11 +260,11 @@ 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
|
||||
@ -318,7 +285,7 @@ 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)) {
|
||||
!['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,379 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden"
|
||||
>
|
||||
<!-- banner image -->
|
||||
<div class="absolute flex top-0 h-fit inset-x-0 z-[-20]">
|
||||
<img :src="bannerUrl" class="w-full h-auto object-cover" />
|
||||
<h1
|
||||
class="absolute inset-x-0 w-fit mx-auto text-center top-32 -translate-y-[50%] text-4xl text-zinc-100 font-bold font-display z-50 p-4 shadow-xl bg-zinc-900/80 rounded-xl"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
|
||||
/>
|
||||
</div>
|
||||
<!-- main page -->
|
||||
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
|
||||
<!-- game toolbar -->
|
||||
<div class="h-full flex flex-row gap-x-4 items-stretch">
|
||||
<GameStatusButton
|
||||
@install="() => installFlow()"
|
||||
@launch="() => launch()"
|
||||
@queue="() => queue()"
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
:href="remoteUrl"
|
||||
target="_blank"
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-zinc-800/50 px-4 font-semibold text-white shadow-sm hover:bg-zinc-800/80 uppercase font-display"
|
||||
>
|
||||
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
|
||||
|
||||
Store
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalTemplate v-model="installFlowOpen">
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Drop will add {{ game.mName }} to the queue to be downloaded.
|
||||
While downloading, Drop may use up a large amount of resources,
|
||||
particularly network bandwidth and CPU utilisation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<div v-if="versionOptions && versionOptions.length > 0">
|
||||
<Listbox as="div" v-model="installVersionIndex">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Version</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate"
|
||||
>{{ versionOptions[installVersionIndex].versionName }}
|
||||
on
|
||||
{{ versionOptions[installVersionIndex].platform }}</span
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(version, versionIdx) in versionOptions"
|
||||
:key="version.versionName"
|
||||
:value="versionIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ version.versionName }} on
|
||||
{{ version.platform }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
There are no supported versions to install. Please contact your
|
||||
server admin or try again later.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Install to</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-800 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="block truncate">{{
|
||||
installDirs[installDir]
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
as="template"
|
||||
v-for="(dir, dirIdx) in installDirs"
|
||||
:key="dir"
|
||||
:value="dirIdx"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-300',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected
|
||||
? 'font-semibold text-zinc-100'
|
||||
: 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ dir }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="text-zinc-400 text-sm mt-2">
|
||||
Add more install directories in
|
||||
<PageWidget to="/settings/downloads">
|
||||
<WrenchIcon class="size-3" />
|
||||
Settings
|
||||
</PageWidget>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="installError" class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ installError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="
|
||||
!(versionOptions && versionOptions.length > 0 && !installDir)
|
||||
"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
>
|
||||
Install
|
||||
</LoadingButton>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
@click="installFlowOpen = false"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const id = route.params.id.toString();
|
||||
|
||||
const { game: rawGame, status } = await useGame(id);
|
||||
const game = ref(rawGame);
|
||||
|
||||
const remoteUrl: string = await invoke("gen_drop_url", {
|
||||
path: `/store/${game.value.id}`,
|
||||
});
|
||||
|
||||
const bannerUrl = await useObject(game.value.mBannerId);
|
||||
|
||||
const installFlowOpen = ref(false);
|
||||
const versionOptions = ref<
|
||||
undefined | Array<{ versionName: string; platform: string }>
|
||||
>();
|
||||
const installDirs = ref<undefined | Array<string>>();
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
installDirs.value = undefined;
|
||||
|
||||
try {
|
||||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
console.log(versionOptions.value);
|
||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
}
|
||||
}
|
||||
|
||||
const installLoading = ref(false);
|
||||
const installError = ref<string | undefined>();
|
||||
const installVersionIndex = ref(0);
|
||||
const installDir = ref(0);
|
||||
async function install() {
|
||||
try {
|
||||
if (!versionOptions.value)
|
||||
throw new Error("Versions have not been loaded");
|
||||
installLoading.value = true;
|
||||
await invoke("download_game", {
|
||||
gameId: game.value.id,
|
||||
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
|
||||
installDir: installDir.value,
|
||||
});
|
||||
installFlowOpen.value = false;
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
}
|
||||
|
||||
installLoading.value = false;
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { id: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: `Couldn't run "${game.value.mName}"`,
|
||||
description: `Drop failed to launch "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function queue() {
|
||||
router.push("/queue");
|
||||
}
|
||||
|
||||
async function uninstall() {
|
||||
await invoke("uninstall_game", { gameId: game.value.id });
|
||||
}
|
||||
|
||||
async function kill() {
|
||||
try {
|
||||
await invoke("kill_game", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: `Couldn't stop "${game.value.mName}"`,
|
||||
description: `Drop failed to stop "${game.value.mName}": ${e}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -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>
|
||||
3687
src-tauri/Cargo.lock
generated
3687
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"
|
||||
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"]
|
||||
version = "0.12.22"
|
||||
default-features = false
|
||||
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user