mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 08:12:44 +10:00
Compare commits
5 Commits
e29d5c8ead
...
42-feature
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e20f3ca9 | |||
| ae6935554f | |||
| 69c71de5cd | |||
| 9b68ebc910 | |||
| beea0505d1 |
23
.github/workflows/clippy.yml
vendored
23
.github/workflows/clippy.yml
vendored
@ -1,23 +0,0 @@
|
|||||||
on: push
|
|
||||||
name: Clippy check
|
|
||||||
jobs:
|
|
||||||
clippy_check:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
checks: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
components: clippy
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/clippy-check@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --manifest-path ./src-tauri/Cargo.toml
|
|
||||||
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@ -22,9 +22,9 @@ jobs:
|
|||||||
args: '--target aarch64-apple-darwin'
|
args: '--target aarch64-apple-darwin'
|
||||||
- platform: 'macos-latest' # for Intel based macs.
|
- platform: 'macos-latest' # for Intel based macs.
|
||||||
args: '--target x86_64-apple-darwin'
|
args: '--target x86_64-apple-darwin'
|
||||||
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||||
args: ''
|
args: ''
|
||||||
- platform: 'ubuntu-22.04-arm'
|
- platform: 'ubuntu-24.04-arm'
|
||||||
args: '--target aarch64-unknown-linux-gnu'
|
args: '--target aarch64-unknown-linux-gnu'
|
||||||
- platform: 'windows-latest'
|
- platform: 'windows-latest'
|
||||||
args: ''
|
args: ''
|
||||||
@ -48,42 +48,12 @@ jobs:
|
|||||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
- 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.
|
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
|
||||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||||
|
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||||
|
|
||||||
- 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
|
- name: install frontend dependencies
|
||||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
@ -91,10 +61,6 @@ jobs:
|
|||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
with:
|
||||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||||
releaseName: 'Auto-release v__VERSION__'
|
releaseName: 'Auto-release v__VERSION__'
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,6 +27,3 @@ dist-ssr
|
|||||||
|
|
||||||
src-tauri/flamegraph.svg
|
src-tauri/flamegraph.svg
|
||||||
src-tauri/perf*
|
src-tauri/perf*
|
||||||
|
|
||||||
/*.AppImage
|
|
||||||
/squashfs-root
|
|
||||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@ -1,6 +1,9 @@
|
|||||||
|
[submodule "drop-base"]
|
||||||
|
path = drop-base
|
||||||
|
url = https://github.com/drop-oss/drop-base
|
||||||
[submodule "src-tauri/tailscale/libtailscale"]
|
[submodule "src-tauri/tailscale/libtailscale"]
|
||||||
path = src-tauri/tailscale/libtailscale
|
path = src-tauri/tailscale/libtailscale
|
||||||
url = https://github.com/tailscale/libtailscale.git
|
url = https://github.com/tailscale/libtailscale.git
|
||||||
[submodule "libs/drop-base"]
|
[submodule "src-tauri/umu/umu-launcher"]
|
||||||
path = libs/drop-base
|
path = src-tauri/umu/umu-launcher
|
||||||
url = https://github.com/drop-oss/drop-base.git
|
url = https://github.com/Open-Wine-Components/umu-launcher.git
|
||||||
|
|||||||
24
README.md
24
README.md
@ -1,21 +1,29 @@
|
|||||||
# Drop Desktop Client
|
# Drop App
|
||||||
|
|
||||||
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
|
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
||||||
|
|
||||||
## Internals
|
## Running
|
||||||
|
Before setting up the drop app, be sure that you have a server set up.
|
||||||
|
The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
|
||||||
|
|
||||||
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
|
## Current features
|
||||||
|
Currently supported are the following features:
|
||||||
|
- Signin (with custom server)
|
||||||
|
- Database registering & recovery
|
||||||
|
- Dynamic library fetching from server
|
||||||
|
- Installing & uninstalling games
|
||||||
|
- Download progress monitoring
|
||||||
|
- Launching / playing games
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
|
|
||||||
|
|
||||||
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
|
Install dependencies with `yarn`
|
||||||
|
|
||||||
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
|
||||||
|
|
||||||
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
|
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
|
||||||
|
|
||||||
e.g. `RUST_LOG=debug yarn tauri dev`
|
e.g. `RUST_LOG=debug yarn tauri dev`
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).
|
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.
|
||||||
@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLoadingIndicator color="#2563eb" />
|
<NuxtLayout class="select-none w-screen h-screen">
|
||||||
<NuxtLayout ref="rootNode" class="select-none w-screen h-screen">
|
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<ModalStack />
|
<ModalStack />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
@ -10,41 +9,29 @@
|
|||||||
import "~/composables/downloads.js";
|
import "~/composables/downloads.js";
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { AppStatus } from "~/types";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useAppState } from "./composables/app-state.js";
|
import { useAppState } from "./composables/app-state.js";
|
||||||
import {
|
import {
|
||||||
initialNavigation,
|
initialNavigation,
|
||||||
setupHooks,
|
setupHooks,
|
||||||
} from "./composables/state-navigation.js";
|
} from "./composables/state-navigation.js";
|
||||||
import { createTVNavigator } from "./composables/tvmode.js";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const state = useAppState();
|
const state = useAppState();
|
||||||
|
try {
|
||||||
|
state.value = JSON.parse(await invoke("fetch_state"));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("failed to parse state", e);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchState() {
|
router.beforeEach(async () => {
|
||||||
try {
|
try {
|
||||||
state.value = JSON.parse(await invoke("fetch_state"));
|
state.value = JSON.parse(await invoke("fetch_state"));
|
||||||
if (!state.value)
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: `App state is: ${state.value}`,
|
|
||||||
fatal: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("failed to parse state", e);
|
console.error("failed to parse state", e);
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await fetchState();
|
|
||||||
|
|
||||||
// This is inefficient but apparently we do it lol
|
|
||||||
router.beforeEach(async () => {
|
|
||||||
await fetchState();
|
|
||||||
});
|
|
||||||
|
|
||||||
const rootNode = ref<HTMLElement>();
|
|
||||||
onMounted(() => {
|
|
||||||
const navigator = createTVNavigator(rootNode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setupHooks();
|
setupHooks();
|
||||||
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 6.5 MiB |
55
build.mjs
55
build.mjs
@ -1,55 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import process from "process";
|
|
||||||
import childProcess from "child_process";
|
|
||||||
import createLogger from "pino";
|
|
||||||
|
|
||||||
const OUTPUT = "./.output";
|
|
||||||
const logger = createLogger({ transport: { target: "pino-pretty" } });
|
|
||||||
|
|
||||||
async function spawn(exec, opts) {
|
|
||||||
const output = childProcess.spawn(exec, { ...opts, shell: true });
|
|
||||||
output.stdout.on("data", (data) => {
|
|
||||||
process.stdout.write(data);
|
|
||||||
});
|
|
||||||
output.stderr.on("data", (data) => {
|
|
||||||
process.stderr.write(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
output.on("error", (err) => reject(err));
|
|
||||||
output.on("exit", () => resolve());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedLibs = ["drop-base/package.json"];
|
|
||||||
|
|
||||||
for (const lib of expectedLibs) {
|
|
||||||
const path = `./libs/${lib}`;
|
|
||||||
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = fs.readdirSync(".").filter((view) => {
|
|
||||||
const expectedPath = `./${view}/package.json`;
|
|
||||||
return fs.existsSync(expectedPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.mkdirSync(OUTPUT, { recursive: true });
|
|
||||||
|
|
||||||
for (const view of views) {
|
|
||||||
const loggerChild = logger.child({});
|
|
||||||
process.chdir(`./${view}`);
|
|
||||||
|
|
||||||
loggerChild.info(`Install deps for "${view}"`);
|
|
||||||
await spawn("yarn");
|
|
||||||
|
|
||||||
loggerChild.info(`Building "${view}"`);
|
|
||||||
await spawn("yarn build", {
|
|
||||||
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
|
|
||||||
});
|
|
||||||
|
|
||||||
process.chdir("..");
|
|
||||||
|
|
||||||
fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
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"
|
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">
|
<div class="py-1">
|
||||||
<MenuItem v-if="showOptions" v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<button
|
<button
|
||||||
@click="() => emit('options')"
|
@click="() => emit('options')"
|
||||||
:class="[
|
:class="[
|
||||||
@ -87,8 +87,6 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
QueueListIcon,
|
QueueListIcon,
|
||||||
ServerIcon,
|
|
||||||
StopIcon,
|
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "@heroicons/vue/20/solid";
|
} from "@heroicons/vue/20/solid";
|
||||||
|
|
||||||
@ -105,18 +103,12 @@ const emit = defineEmits<{
|
|||||||
(e: "uninstall"): void;
|
(e: "uninstall"): void;
|
||||||
(e: "kill"): void;
|
(e: "kill"): void;
|
||||||
(e: "options"): void;
|
(e: "options"): void;
|
||||||
(e: "resume"): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showDropdown = computed(
|
const showDropdown = computed(
|
||||||
() =>
|
() =>
|
||||||
props.status.type === GameStatusEnum.Installed ||
|
props.status.type === GameStatusEnum.Installed ||
|
||||||
props.status.type === GameStatusEnum.SetupRequired ||
|
props.status.type === GameStatusEnum.SetupRequired
|
||||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
|
||||||
);
|
|
||||||
|
|
||||||
const showOptions = computed(
|
|
||||||
() => props.status.type === GameStatusEnum.Installed
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles: { [key in GameStatusEnum]: string } = {
|
const styles: { [key in GameStatusEnum]: string } = {
|
||||||
@ -126,8 +118,6 @@ const styles: { [key in GameStatusEnum]: string } = {
|
|||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.Downloading]:
|
[GameStatusEnum.Downloading]:
|
||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"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]:
|
[GameStatusEnum.SetupRequired]:
|
||||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
||||||
[GameStatusEnum.Installed]:
|
[GameStatusEnum.Installed]:
|
||||||
@ -138,46 +128,38 @@ const styles: { [key in GameStatusEnum]: string } = {
|
|||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.Running]:
|
[GameStatusEnum.Running]:
|
||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"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 } = {
|
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Remote]: "Install",
|
[GameStatusEnum.Remote]: "Install",
|
||||||
[GameStatusEnum.Queued]: "Queued",
|
[GameStatusEnum.Queued]: "Queued",
|
||||||
[GameStatusEnum.Downloading]: "Downloading",
|
[GameStatusEnum.Downloading]: "Downloading",
|
||||||
[GameStatusEnum.Validating]: "Validating",
|
|
||||||
[GameStatusEnum.SetupRequired]: "Setup",
|
[GameStatusEnum.SetupRequired]: "Setup",
|
||||||
[GameStatusEnum.Installed]: "Play",
|
[GameStatusEnum.Installed]: "Play",
|
||||||
[GameStatusEnum.Updating]: "Updating",
|
[GameStatusEnum.Updating]: "Updating",
|
||||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||||
[GameStatusEnum.Running]: "Stop",
|
[GameStatusEnum.Running]: "Stop",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "Resume",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||||
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Queued]: QueueListIcon,
|
[GameStatusEnum.Queued]: QueueListIcon,
|
||||||
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Validating]: ServerIcon,
|
|
||||||
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
||||||
[GameStatusEnum.Installed]: PlayIcon,
|
[GameStatusEnum.Installed]: PlayIcon,
|
||||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||||
[GameStatusEnum.Running]: StopIcon,
|
[GameStatusEnum.Running]: PlayIcon,
|
||||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||||
[GameStatusEnum.Remote]: () => emit("install"),
|
[GameStatusEnum.Remote]: () => emit("install"),
|
||||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||||
[GameStatusEnum.Validating]: () => emit("queue"),
|
|
||||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||||
[GameStatusEnum.Uninstalling]: () => {},
|
[GameStatusEnum.Uninstalling]: () => {},
|
||||||
[GameStatusEnum.Running]: () => emit("kill"),
|
[GameStatusEnum.Running]: () => emit("kill"),
|
||||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
<component class="h-5" :is="item.icon" />
|
<component class="h-5" :is="item.icon" />
|
||||||
</HeaderWidget>
|
</HeaderWidget>
|
||||||
</li>
|
</li>
|
||||||
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
<OfflineHeaderWidget v-if="state.status === AppStatus.Offline" />
|
||||||
<HeaderUserWidget />
|
<HeaderUserWidget />
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
<Menu v-if="state.user" as="div" class="relative inline-block">
|
||||||
<MenuButton>
|
<MenuButton>
|
||||||
<HeaderWidget>
|
<HeaderWidget>
|
||||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||||
>
|
>
|
||||||
<div class="flex-col gap-y-2">
|
<PanelWidget class="flex-col gap-y-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/id/me"
|
to="/id/me"
|
||||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||||
@ -37,7 +37,7 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||||
<div class="flex flex-col mb-1">
|
<div class="flex flex-col mb-1">
|
||||||
<MenuItem v-if="state.user.admin" v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<a
|
<a
|
||||||
:href="adminUrl"
|
:href="adminUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PanelWidget>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
</transition>
|
</transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
@ -87,7 +87,7 @@ router.afterEach(() => {
|
|||||||
|
|
||||||
const state = useAppState();
|
const state = useAppState();
|
||||||
const profilePictureUrl: string = await useObject(
|
const profilePictureUrl: string = await useObject(
|
||||||
state.value?.user?.profilePictureObjectId ?? ""
|
state.value.user?.profilePictureObjectId ?? ""
|
||||||
);
|
);
|
||||||
const adminUrl: string = await invoke("gen_drop_url", {
|
const adminUrl: string = await invoke("gen_drop_url", {
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
@ -13,7 +13,11 @@
|
|||||||
<div class="max-w-lg">
|
<div class="max-w-lg">
|
||||||
<slot />
|
<slot />
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<div>
|
<button
|
||||||
|
@click="() => authWrapper_wrapper()"
|
||||||
|
:disabled="loading"
|
||||||
|
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
||||||
|
>
|
||||||
<div v-if="loading" role="status">
|
<div v-if="loading" role="status">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -33,19 +37,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="inline-flex gap-x-8 items-center" v-else>
|
<span v-else>
|
||||||
<button
|
Sign in with your browser <span aria-hidden="true">→</span>
|
||||||
@click="() => authWrapper_wrapper()"
|
|
||||||
:disabled="loading"
|
|
||||||
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
|
|
||||||
>
|
|
||||||
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
|
|
||||||
</button>
|
|
||||||
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
|
||||||
Use a code →
|
|
||||||
</NuxtLink>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div class="mt-5" v-if="offerManual">
|
<div class="mt-5" v-if="offerManual">
|
||||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||||
@ -126,7 +121,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
177
components/LibrarySearch.vue
Normal file
177
components/LibrarySearch.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="relative mb-3 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>
|
||||||
|
|
||||||
|
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="nav in filteredNavigation"
|
||||||
|
:key="nav.id"
|
||||||
|
:class="[
|
||||||
|
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||||
|
nav.index === currentNavigation
|
||||||
|
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||||
|
: nav.isInstalled.value
|
||||||
|
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||||
|
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||||
|
]"
|
||||||
|
:href="nav.route"
|
||||||
|
>
|
||||||
|
<div class="flex items-center w-full gap-x-3">
|
||||||
|
<div
|
||||||
|
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||||
|
:src="icons[nav.id]"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<p
|
||||||
|
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
||||||
|
>
|
||||||
|
{{ nav.label }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-xs font-medium"
|
||||||
|
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
||||||
|
>
|
||||||
|
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
|
||||||
|
import { TransitionGroup } from "vue";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
// Style information
|
||||||
|
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||||
|
[GameStatusEnum.Installed]: "text-green-500",
|
||||||
|
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||||
|
[GameStatusEnum.Running]: "text-green-500",
|
||||||
|
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||||
|
[GameStatusEnum.Queued]: "text-blue-500",
|
||||||
|
[GameStatusEnum.Updating]: "text-blue-500",
|
||||||
|
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||||
|
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||||
|
};
|
||||||
|
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||||
|
[GameStatusEnum.Remote]: "Not installed",
|
||||||
|
[GameStatusEnum.Queued]: "Queued",
|
||||||
|
[GameStatusEnum.Downloading]: "Downloading...",
|
||||||
|
[GameStatusEnum.Installed]: "Installed",
|
||||||
|
[GameStatusEnum.Updating]: "Updating...",
|
||||||
|
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||||
|
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||||
|
[GameStatusEnum.Running]: "Running",
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
|
||||||
|
const games: {
|
||||||
|
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||||
|
} = {};
|
||||||
|
const icons: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||||
|
|
||||||
|
async function calculateGames() {
|
||||||
|
rawGames.value = await invoke("fetch_library");
|
||||||
|
for (const game of rawGames.value) {
|
||||||
|
if (games[game.id]) continue;
|
||||||
|
games[game.id] = await useGame(game.id);
|
||||||
|
}
|
||||||
|
for (const game of rawGames.value) {
|
||||||
|
if (icons[game.id]) continue;
|
||||||
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await calculateGames();
|
||||||
|
|
||||||
|
const navigation = computed(() =>
|
||||||
|
rawGames.value.map((game) => {
|
||||||
|
const status = games[game.id].status;
|
||||||
|
|
||||||
|
const isInstalled = computed(
|
||||||
|
() =>
|
||||||
|
status.value.type == GameStatusEnum.Installed ||
|
||||||
|
status.value.type == GameStatusEnum.SetupRequired
|
||||||
|
);
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
label: game.mName,
|
||||||
|
route: `/library/${game.id}`,
|
||||||
|
prefix: `/library/${game.id}`,
|
||||||
|
isInstalled,
|
||||||
|
id: game.id,
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
|
||||||
|
navigation.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredNavigation = computed(() => {
|
||||||
|
if (!searchQuery.value)
|
||||||
|
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
return navigation.value
|
||||||
|
.filter((nav) => nav.label.toLowerCase().includes(query))
|
||||||
|
.map((e, i) => ({ ...e, index: i }));
|
||||||
|
});
|
||||||
|
|
||||||
|
listen("update_library", async (event) => {
|
||||||
|
console.log("Updating library");
|
||||||
|
let oldNavigation = navigation.value[currentNavigation.value];
|
||||||
|
await calculateGames();
|
||||||
|
recalculateNavigation();
|
||||||
|
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
||||||
|
console.log("Triggered");
|
||||||
|
router.push("/library");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-move,
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||||
>
|
>
|
||||||
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
||||||
<Wordmark class="mt-1" />
|
<Wordmark class="mt-1" />
|
||||||
3
composables/app-state.ts
Normal file
3
composables/app-state.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { AppState } from "~/types";
|
||||||
|
|
||||||
|
export const useAppState = () => useState<AppState>("state");
|
||||||
@ -32,5 +32,3 @@ listen("update_stats", (event) => {
|
|||||||
const stats = useStatsState();
|
const stats = useStatsState();
|
||||||
stats.value = event.payload as StatsState;
|
stats.value = event.payload as StatsState;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
|
|
||||||
86
composables/game.ts
Normal file
86
composables/game.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { type Game, type GameStatus as DownloadStatus, type GameStatusEnum as DownloadStatusEnum, type GameVersion, type DownloadableMetadata, DownloadableType } from "~/types";
|
||||||
|
|
||||||
|
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
|
||||||
|
{};
|
||||||
|
|
||||||
|
const downloadStatusRegistry: Map<DownloadableMetadata, Ref<DownloadStatus>> = new Map();
|
||||||
|
|
||||||
|
type OptionDownloadStatus = { [key in DownloadStatusEnum]: { version_name?: string } };
|
||||||
|
export type SerializedDownloadStatus = [
|
||||||
|
{ type: DownloadStatusEnum },
|
||||||
|
OptionDownloadStatus | null
|
||||||
|
];
|
||||||
|
|
||||||
|
export const parseStatus = (status: SerializedDownloadStatus): DownloadStatus => {
|
||||||
|
console.log(status);
|
||||||
|
if (status[0]) {
|
||||||
|
return {
|
||||||
|
type: status[0].type,
|
||||||
|
};
|
||||||
|
} else if (status[1]) {
|
||||||
|
const [[gameStatus, options]] = Object.entries(status[1]);
|
||||||
|
return {
|
||||||
|
type: gameStatus as DownloadStatusEnum,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error("No game status");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStatus = (meta: DownloadableMetadata) => {
|
||||||
|
return downloadStatusRegistry.get(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGame = async (gameId: string) => {
|
||||||
|
const data: {
|
||||||
|
game: Game;
|
||||||
|
status: SerializedDownloadStatus;
|
||||||
|
version?: GameVersion;
|
||||||
|
} = await invoke("fetch_game", {
|
||||||
|
gameId,
|
||||||
|
});
|
||||||
|
const meta = {
|
||||||
|
id: gameId,
|
||||||
|
version: data.version?.versionName,
|
||||||
|
downloadType: DownloadableType.Game
|
||||||
|
} satisfies DownloadableMetadata;
|
||||||
|
if (!gameRegistry[gameId]) {
|
||||||
|
|
||||||
|
gameRegistry[gameId] = { game: data.game, version: data.version };
|
||||||
|
}
|
||||||
|
if (!downloadStatusRegistry.has(meta)) {
|
||||||
|
downloadStatusRegistry.set(meta, ref(parseStatus(data.status)));
|
||||||
|
|
||||||
|
listen(`update_game/${gameId}`, (event) => {
|
||||||
|
const payload: {
|
||||||
|
status: SerializedDownloadStatus;
|
||||||
|
version?: GameVersion;
|
||||||
|
} = event.payload as any;
|
||||||
|
|
||||||
|
downloadStatusRegistry.get(meta)!.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 = downloadStatusRegistry.get(meta)!;
|
||||||
|
return { ...game, status };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FrontendGameConfiguration = {
|
||||||
|
launchString: string;
|
||||||
|
};
|
||||||
@ -1,11 +1,9 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { data } from "autoprefixer";
|
import { data } from "autoprefixer";
|
||||||
import { AppStatus, type AppState } from "~/types";
|
import { AppStatus, type AppState } from "~/types";
|
||||||
|
|
||||||
export function setupHooks() {
|
export function setupHooks() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state = useAppState();
|
|
||||||
|
|
||||||
listen("auth/processing", (event) => {
|
listen("auth/processing", (event) => {
|
||||||
router.push("/auth/processing");
|
router.push("/auth/processing");
|
||||||
@ -17,9 +15,8 @@ export function setupHooks() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("auth/finished", async (event) => {
|
listen("auth/finished", (event) => {
|
||||||
router.push("/library");
|
router.push("/store");
|
||||||
state.value = JSON.parse(await invoke("fetch_state"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("download_error", (event) => {
|
listen("download_error", (event) => {
|
||||||
@ -30,31 +27,12 @@ export function setupHooks() {
|
|||||||
description: `Drop encountered an error while downloading your game: "${(
|
description: `Drop encountered an error while downloading your game: "${(
|
||||||
event.payload as unknown as string
|
event.payload as unknown as string
|
||||||
).toString()}"`,
|
).toString()}"`,
|
||||||
buttonText: "Close",
|
buttonText: "Close"
|
||||||
},
|
},
|
||||||
(e, c) => c()
|
(e, c) => c()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is for errors that (we think) aren't our fault
|
|
||||||
listen("launch_external_error", (event) => {
|
|
||||||
createModal(
|
|
||||||
ModalType.Confirmation,
|
|
||||||
{
|
|
||||||
title: "Did something go wrong?",
|
|
||||||
description:
|
|
||||||
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
|
|
||||||
buttonText: "Open",
|
|
||||||
},
|
|
||||||
async (e, c) => {
|
|
||||||
if (e == "confirm") {
|
|
||||||
await invoke("open_process_logs", { gameId: event.payload });
|
|
||||||
}
|
|
||||||
c();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
document.addEventListener("contextmenu", (event) => {
|
document.addEventListener("contextmenu", (event) => {
|
||||||
@ -65,13 +43,7 @@ export function setupHooks() {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialNavigation(state: ReturnType<typeof useAppState>) {
|
export function initialNavigation(state: Ref<AppState>) {
|
||||||
if (!state.value)
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: "App state not valid",
|
|
||||||
fatal: true,
|
|
||||||
});
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
switch (state.value.status) {
|
switch (state.value.status) {
|
||||||
@ -88,6 +60,6 @@ export function initialNavigation(state: ReturnType<typeof useAppState>) {
|
|||||||
router.push("/error/serverunavailable");
|
router.push("/error/serverunavailable");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
router.push("/library");
|
router.push("/store");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
drop-base
Submodule
1
drop-base
Submodule
Submodule drop-base added at 26698e5b06
@ -7,7 +7,6 @@
|
|||||||
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"
|
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" />
|
<Logo class="h-10 w-auto sm:h-12" />
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<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"
|
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"
|
||||||
Submodule libs/drop-base deleted from 04125e89be
@ -1,303 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col h-full">
|
|
||||||
<div class="mb-3 inline-flex gap-x-2">
|
|
||||||
<div
|
|
||||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
|
||||||
>
|
|
||||||
<MagnifyingGlassIcon
|
|
||||||
class="h-5 w-5 text-zinc-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="searchQuery"
|
|
||||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
|
||||||
placeholder="Search library..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
@click="() => calculateGames(true, true)"
|
|
||||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
|
||||||
>
|
|
||||||
<ArrowPathIcon class="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
|
||||||
<Disclosure
|
|
||||||
as="div"
|
|
||||||
v-for="(nav, navIndex) in filteredNavigation"
|
|
||||||
:key="nav.id"
|
|
||||||
class="first:pt-0 last:pb-0"
|
|
||||||
v-slot="{ open }"
|
|
||||||
:default-open="nav.deft"
|
|
||||||
>
|
|
||||||
<dt>
|
|
||||||
<DisclosureButton
|
|
||||||
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
<span class="text-sm font-semibold font-display">{{
|
|
||||||
nav.name
|
|
||||||
}}</span>
|
|
||||||
<span class="ml-6 flex h-7 items-center">
|
|
||||||
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
|
|
||||||
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</DisclosureButton>
|
|
||||||
</dt>
|
|
||||||
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
|
|
||||||
<NuxtLink
|
|
||||||
v-for="item in nav.items"
|
|
||||||
:key="nav.id"
|
|
||||||
:class="[
|
|
||||||
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
|
||||||
currentNavigation == item.id
|
|
||||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
|
||||||
: item.isInstalled.value
|
|
||||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
|
||||||
]"
|
|
||||||
:href="item.route"
|
|
||||||
>
|
|
||||||
<div class="flex items-center w-full gap-x-2">
|
|
||||||
<div
|
|
||||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
|
|
||||||
:src="icons[item.id]"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex items-center gap-x-2">
|
|
||||||
<p
|
|
||||||
class="text-sm whitespace-nowrap font-display font-semibold"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="truncate text-[10px] font-bold uppercase font-display"
|
|
||||||
:class="[
|
|
||||||
gameStatusTextStyle[games[item.id].status.value.type],
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ gameStatusText[games[item.id].status.value.type] }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</TransitionGroup>
|
|
||||||
<div
|
|
||||||
v-if="loading"
|
|
||||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
|
||||||
>
|
|
||||||
<div role="status">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
|
|
||||||
import {
|
|
||||||
ArrowPathIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
MinusSmallIcon,
|
|
||||||
PlusSmallIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import {
|
|
||||||
GameStatusEnum,
|
|
||||||
type Collection as Collection,
|
|
||||||
type Game,
|
|
||||||
type GameStatus,
|
|
||||||
} from "~/types";
|
|
||||||
import { TransitionGroup } from "vue";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
|
|
||||||
// Style information
|
|
||||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
|
||||||
[GameStatusEnum.Installed]: "text-green-500",
|
|
||||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
|
||||||
[GameStatusEnum.Validating]: "text-blue-300",
|
|
||||||
[GameStatusEnum.Running]: "text-green-500",
|
|
||||||
[GameStatusEnum.Remote]: "text-zinc-700",
|
|
||||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
|
||||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
|
||||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
|
||||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
|
||||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
|
||||||
};
|
|
||||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
|
||||||
[GameStatusEnum.Remote]: "Not installed",
|
|
||||||
[GameStatusEnum.Queued]: "Queued",
|
|
||||||
[GameStatusEnum.Downloading]: "Downloading...",
|
|
||||||
[GameStatusEnum.Validating]: "Validating...",
|
|
||||||
[GameStatusEnum.Installed]: "Installed",
|
|
||||||
[GameStatusEnum.Updating]: "Updating...",
|
|
||||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
|
||||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
|
||||||
[GameStatusEnum.Running]: "Running",
|
|
||||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const searchQuery = ref("");
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const games: {
|
|
||||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
|
||||||
} = {};
|
|
||||||
const icons: { [key: string]: string } = {};
|
|
||||||
|
|
||||||
const collections: Ref<Collection[]> = ref([]);
|
|
||||||
|
|
||||||
async function calculateGames(clearAll = false, forceRefresh = false) {
|
|
||||||
if (clearAll) {
|
|
||||||
collections.value = [];
|
|
||||||
loading.value = true;
|
|
||||||
}
|
|
||||||
// If we update immediately, the navigation gets re-rendered before we
|
|
||||||
// add all the necessary state, and it freaks tf out
|
|
||||||
const newGames = await invoke<Game[]>("fetch_library", {
|
|
||||||
hardRefresh: forceRefresh,
|
|
||||||
});
|
|
||||||
const otherCollections = await invoke<Collection[]>("fetch_collections", {
|
|
||||||
hardRefresh: forceRefresh,
|
|
||||||
});
|
|
||||||
const allGames = [
|
|
||||||
...newGames,
|
|
||||||
...otherCollections
|
|
||||||
.map((e) => e.entries)
|
|
||||||
.flat()
|
|
||||||
.map((e) => e.game),
|
|
||||||
].filter((v, i, a) => a.indexOf(v) === i);
|
|
||||||
|
|
||||||
for (const game of allGames) {
|
|
||||||
if (games[game.id]) continue;
|
|
||||||
games[game.id] = await useGame(game.id);
|
|
||||||
}
|
|
||||||
for (const game of allGames) {
|
|
||||||
if (icons[game.id]) continue;
|
|
||||||
icons[game.id] = await useObject(game.mIconObjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const libraryCollection = {
|
|
||||||
id: "library",
|
|
||||||
name: "Library",
|
|
||||||
isDefault: true,
|
|
||||||
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
|
|
||||||
} satisfies Collection;
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
collections.value = [libraryCollection, ...otherCollections];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait up to 300 ms for the library to load, otherwise
|
|
||||||
// show the loading state while we while
|
|
||||||
await new Promise<void>((r) => {
|
|
||||||
let hasResolved = false;
|
|
||||||
const resolveFunc = () => {
|
|
||||||
if (!hasResolved) r();
|
|
||||||
hasResolved = true;
|
|
||||||
};
|
|
||||||
calculateGames(true).then(resolveFunc);
|
|
||||||
setTimeout(resolveFunc, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigation = computed(() =>
|
|
||||||
collections.value.map((collection) => {
|
|
||||||
const items = collection.entries.map(({ game }) => {
|
|
||||||
const status = games[game.id].status;
|
|
||||||
|
|
||||||
const isInstalled = computed(
|
|
||||||
() => status.value.type != GameStatusEnum.Remote
|
|
||||||
);
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
label: game.mName,
|
|
||||||
route: `/library/${game.id}`,
|
|
||||||
prefix: `/library/${game.id}`,
|
|
||||||
isInstalled,
|
|
||||||
id: game.id,
|
|
||||||
};
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: collection.id,
|
|
||||||
name: collection.name,
|
|
||||||
deft: collection.isDefault,
|
|
||||||
items,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const currentNavigation = computed(() => {
|
|
||||||
return route.path.slice("/library/".length);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredNavigation = computed(() => {
|
|
||||||
if (!searchQuery.value)
|
|
||||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
|
||||||
return navigation.value
|
|
||||||
.map((c) => ({
|
|
||||||
...c,
|
|
||||||
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
|
|
||||||
}))
|
|
||||||
.filter((e) => e.items.length > 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("update_library", async (event) => {
|
|
||||||
console.log("Updating library");
|
|
||||||
let oldNavigation = currentNavigation.value;
|
|
||||||
await calculateGames();
|
|
||||||
if (oldNavigation !== currentNavigation.value) {
|
|
||||||
router.push("/library");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.list-move,
|
|
||||||
.list-enter-active,
|
|
||||||
.list-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-enter-from,
|
|
||||||
.list-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-leave-active {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import type { AppState } from "~/types";
|
|
||||||
|
|
||||||
export const useAppState = () => useState<AppState | undefined>("state");
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
|
|
||||||
|
|
||||||
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
|
|
||||||
{};
|
|
||||||
|
|
||||||
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
|
|
||||||
|
|
||||||
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
|
|
||||||
export type SerializedGameStatus = [
|
|
||||||
{ type: GameStatusEnum },
|
|
||||||
OptionGameStatus | null
|
|
||||||
];
|
|
||||||
|
|
||||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
|
||||||
if (status[0]) {
|
|
||||||
return {
|
|
||||||
type: status[0].type,
|
|
||||||
};
|
|
||||||
} else if (status[1]) {
|
|
||||||
const [[gameStatus, options]] = Object.entries(status[1]);
|
|
||||||
return {
|
|
||||||
type: gameStatus as GameStatusEnum,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error("No game status");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGame = async (gameId: string) => {
|
|
||||||
if (!gameRegistry[gameId]) {
|
|
||||||
const data: {
|
|
||||||
game: Game;
|
|
||||||
status: SerializedGameStatus;
|
|
||||||
version?: GameVersion;
|
|
||||||
} = await invoke("fetch_game", {
|
|
||||||
gameId,
|
|
||||||
});
|
|
||||||
gameRegistry[gameId] = { game: data.game, version: data.version };
|
|
||||||
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;
|
|
||||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* I am not super happy about this.
|
|
||||||
*
|
|
||||||
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
|
|
||||||
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
|
|
||||||
* on transient state updates.
|
|
||||||
*/
|
|
||||||
if (payload.version) {
|
|
||||||
gameRegistry[gameId].version = payload.version;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const game = gameRegistry[gameId];
|
|
||||||
const status = gameStatusRegistry[gameId];
|
|
||||||
return { ...game, status };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FrontendGameConfiguration = {
|
|
||||||
launchString: string;
|
|
||||||
};
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
const NAVIGATE_MODIFIED_PROP = "tvnav-id";
|
|
||||||
const NAVIGATE_INTERACT_ID = "tvnav-iid";
|
|
||||||
|
|
||||||
const Directions = ["left", "right", "up", "down"] as const;
|
|
||||||
type Direction = (typeof Directions)[number];
|
|
||||||
|
|
||||||
const NAVIGATE_LEFT_ID = "tvnav-left";
|
|
||||||
const NAVIGATE_RIGHT_ID = "tvnav-right";
|
|
||||||
const NAVIGATE_UP_ID = "tvnav-up";
|
|
||||||
const NAVIGATE_DOWN_ID = "tvnav-down";
|
|
||||||
|
|
||||||
interface NavigationJump {
|
|
||||||
distance: number;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TVModeNavigator {
|
|
||||||
private rootNode: Ref<HTMLElement | undefined>;
|
|
||||||
private navigationNodes: Map<string, Array<Element>> = new Map();
|
|
||||||
|
|
||||||
constructor(rootNode: Ref<HTMLElement | undefined>) {
|
|
||||||
this.rootNode = rootNode;
|
|
||||||
|
|
||||||
const thisRef = this;
|
|
||||||
const observer = new MutationObserver((v, k) => {
|
|
||||||
this.onMutation(thisRef, v, k);
|
|
||||||
});
|
|
||||||
observer.observe(document.getRootNode(), {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
recursivelyFindInteractable(element: Element): Array<Element> {
|
|
||||||
const elements = [];
|
|
||||||
for (const child of element.children) {
|
|
||||||
if (!child) continue;
|
|
||||||
if (child instanceof HTMLAnchorElement) {
|
|
||||||
elements.push(child);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (child instanceof HTMLButtonElement) {
|
|
||||||
elements.push(child);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save ourselves a function call
|
|
||||||
if (child.children.length > 0) {
|
|
||||||
elements.push(...this.recursivelyFindInteractable(child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInteractionId(element: Element) {
|
|
||||||
const id = element.getAttribute(NAVIGATE_INTERACT_ID);
|
|
||||||
if (id) return id;
|
|
||||||
const newId = crypto.randomUUID();
|
|
||||||
element.setAttribute(NAVIGATE_INTERACT_ID, newId);
|
|
||||||
return newId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNavJumpKey(direction: Direction) {
|
|
||||||
switch (direction) {
|
|
||||||
case "down":
|
|
||||||
return NAVIGATE_DOWN_ID;
|
|
||||||
case "left":
|
|
||||||
return NAVIGATE_LEFT_ID;
|
|
||||||
case "right":
|
|
||||||
return NAVIGATE_RIGHT_ID;
|
|
||||||
case "up":
|
|
||||||
return NAVIGATE_UP_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw "Invalid direction";
|
|
||||||
}
|
|
||||||
|
|
||||||
getNavJump(
|
|
||||||
element: Element,
|
|
||||||
direction: Direction
|
|
||||||
): NavigationJump | undefined {
|
|
||||||
const key = this.getNavJumpKey(direction);
|
|
||||||
const value = element.getAttribute(key);
|
|
||||||
if (!value) return undefined;
|
|
||||||
const [id, distance] = value.split("/");
|
|
||||||
return {
|
|
||||||
distance: parseFloat(distance),
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onMutation(
|
|
||||||
self: TVModeNavigator,
|
|
||||||
mutationlist: Array<MutationRecord>,
|
|
||||||
observer: unknown
|
|
||||||
) {
|
|
||||||
for (const mutation of mutationlist) {
|
|
||||||
for (const node of mutation.removedNodes) {
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
||||||
const el = node as Element;
|
|
||||||
const id = el.getAttribute(NAVIGATE_MODIFIED_PROP);
|
|
||||||
if (id) {
|
|
||||||
self.navigationNodes.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const node of mutation.addedNodes) {
|
|
||||||
if (!node) continue;
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
||||||
const el = node as Element;
|
|
||||||
|
|
||||||
const existingId = el.getAttribute(NAVIGATE_MODIFIED_PROP);
|
|
||||||
if (existingId) {
|
|
||||||
self.navigationNodes.delete(existingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const interactiveNodes = self.recursivelyFindInteractable(el);
|
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
el.setAttribute(NAVIGATE_MODIFIED_PROP, id);
|
|
||||||
|
|
||||||
self.navigationNodes.set(id, interactiveNodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const interactiveElements = this.navigationNodes.values().toArray().flat();
|
|
||||||
for (const element of interactiveElements) {
|
|
||||||
const currentId = self.getInteractionId(element);
|
|
||||||
const directionJumps: Map<Direction, NavigationJump> = new Map();
|
|
||||||
for (const direction of Directions) {
|
|
||||||
const jump = self.getNavJump(element, direction);
|
|
||||||
if (jump) {
|
|
||||||
directionJumps.set(direction, jump);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerX = element.clientLeft;
|
|
||||||
const upperX = element.clientLeft + element.clientWidth;
|
|
||||||
const lowerY = element.clientTop;
|
|
||||||
const upperY = element.clientTop + element.clientHeight;
|
|
||||||
|
|
||||||
for (const otherElement of interactiveElements) {
|
|
||||||
const otherId = self.getInteractionId(otherElement);
|
|
||||||
if (otherId == currentId) continue; // Skip us
|
|
||||||
|
|
||||||
const otherLowerX = otherElement.clientLeft;
|
|
||||||
const otherUpperX = otherElement.clientLeft + otherElement.clientWidth;
|
|
||||||
const otherLowerY = otherElement.clientTop;
|
|
||||||
const otherUpperY = otherElement.clientTop + otherElement.clientHeight;
|
|
||||||
|
|
||||||
for (const direction of Directions) {
|
|
||||||
let jump;
|
|
||||||
switch (direction) {
|
|
||||||
case "down":
|
|
||||||
const leeway =
|
|
||||||
0.1 * (element.clientWidth + otherElement.clientWidth);
|
|
||||||
if (
|
|
||||||
lowerX - leeway < otherUpperX ||
|
|
||||||
upperX + leeway > otherLowerX
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
jump = {
|
|
||||||
id: otherId,
|
|
||||||
distance: upperY - otherUpperY,
|
|
||||||
} satisfies NavigationJump;
|
|
||||||
console.log(jump);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTVNavigator = (rootNode: Ref<HTMLElement | undefined>) =>
|
|
||||||
new TVModeNavigator(rootNode);
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "view",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.3.3",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt generate",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"tauri": "tauri"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@headlessui/vue": "^1.7.23",
|
|
||||||
"@heroicons/vue": "^2.1.5",
|
|
||||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
|
||||||
"@tauri-apps/api": "^2.7.0",
|
|
||||||
"koa": "^2.16.1",
|
|
||||||
"markdown-it": "^14.1.0",
|
|
||||||
"micromark": "^4.0.1",
|
|
||||||
"nuxt": "^3.16.0",
|
|
||||||
"scss": "^0.2.4",
|
|
||||||
"vue-router": "latest",
|
|
||||||
"vuedraggable": "^4.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
|
||||||
"@types/markdown-it": "^14.1.2",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"postcss": "^8.4.47",
|
|
||||||
"sass-embedded": "^1.79.4",
|
|
||||||
"tailwindcss": "^3.4.13",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vue-tsc": "^2.2.10"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-full w-full flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<div class="text-center">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Device authorization
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-md mx-auto">
|
|
||||||
Open Drop on another one of your devices, and use your account
|
|
||||||
dropdown to "Authorize client", and enter the code below.
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
|
|
||||||
>
|
|
||||||
<span v-for="letter in code.split('')">{{ letter }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
|
||||||
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
|
|
||||||
><span aria-hidden="true">←</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>
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow w-full h-full flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<WrenchScrewdriverIcon
|
|
||||||
class="h-12 w-12 text-blue-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="mt-3 text-center sm:mt-5">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Under construction
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-lg">
|
|
||||||
This page hasn't been implemented yet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
WrenchScrewdriverIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
</script>
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow w-full h-full flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<WrenchScrewdriverIcon
|
|
||||||
class="h-12 w-12 text-blue-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="mt-3 text-center sm:mt-5">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Under construction
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-lg">
|
|
||||||
This page hasn't been implemented yet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
WrenchScrewdriverIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
</script>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow w-full h-full flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<WrenchScrewdriverIcon
|
|
||||||
class="h-12 w-12 text-blue-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="mt-3 text-center sm:mt-5">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Under construction
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-lg">
|
|
||||||
This page hasn't been implemented yet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { WrenchScrewdriverIcon } from "@heroicons/vue/20/solid";
|
|
||||||
</script>
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow w-full h-full flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<BuildingStorefrontIcon
|
|
||||||
class="h-12 w-12 text-blue-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="mt-3 text-center sm:mt-5">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Store not supported in client
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-lg">
|
|
||||||
Currently, Drop requires you to view the store in your browser.
|
|
||||||
Please click the button below to open it in your default browser.
|
|
||||||
</p>
|
|
||||||
<NuxtLink
|
|
||||||
:href="storeUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
|
||||||
>
|
|
||||||
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ArrowTopRightOnSquareIcon,
|
|
||||||
BuildingStorefrontIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
|
|
||||||
</script>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
|
||||||
"exclude": ["src-tauri/**/*"]
|
|
||||||
}
|
|
||||||
8091
main/yarn.lock
8091
main/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -13,9 +13,5 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
extends: [["../libs/drop-base"]],
|
extends: [["./drop-base"]],
|
||||||
|
|
||||||
app: {
|
|
||||||
baseURL: "/main",
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
@ -1,22 +0,0 @@
|
|||||||
## This script is largely useless, because there's not much we can do about AppImage size
|
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
|
|
||||||
# build tauri apps
|
|
||||||
# NO_STRIP=true yarn tauri build -- --verbose
|
|
||||||
|
|
||||||
# unpack appimage
|
|
||||||
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)
|
|
||||||
"$APPIMAGE" --appimage-extract
|
|
||||||
|
|
||||||
# strip binary
|
|
||||||
APPIMAGE_UNPACK="./squashfs-root"
|
|
||||||
find $APPIMAGE_UNPACK -type f -exec strip -s {} \;
|
|
||||||
|
|
||||||
APPIMAGETOOL=$(echo "obsolete-appimagetool-$ARCH.AppImage")
|
|
||||||
wget -O $APPIMAGETOOL "https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGETOOL"
|
|
||||||
chmod +x $APPIMAGETOOL
|
|
||||||
|
|
||||||
APPIMAGE_OUTPUT=$(./$APPIMAGETOOL $APPIMAGE_UNPACK | grep ".AppImage" | grep squashfs-root | awk '{ print $6 }')
|
|
||||||
|
|
||||||
mv $APPIMAGE_OUTPUT "$APPIMAGE"
|
|
||||||
45
package.json
45
package.json
@ -1,22 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.3.0-rc-2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ./build.mjs",
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.7.0",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
"@heroicons/vue": "^2.1.5",
|
||||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
"@tauri-apps/api": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-os": "^2.3.0",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||||
"pino": "^9.7.0",
|
"@tauri-apps/plugin-os": "~2",
|
||||||
"pino-pretty": "^13.1.1"
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
|
"koa": "^2.16.1",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"micromark": "^4.0.1",
|
||||||
|
"nuxt": "^3.16.0",
|
||||||
|
"scss": "^0.2.4",
|
||||||
|
"vue": "latest",
|
||||||
|
"vue-router": "latest",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.7.1"
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
}
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@tauri-apps/cli": ">=2.0.0",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"sass-embedded": "^1.79.4",
|
||||||
|
"tailwindcss": "^3.4.13",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,6 @@
|
|||||||
@uninstall="() => uninstall()"
|
@uninstall="() => uninstall()"
|
||||||
@kill="() => kill()"
|
@kill="() => kill()"
|
||||||
@options="() => (configureModalOpen = true)"
|
@options="() => (configureModalOpen = true)"
|
||||||
@resume="() => resumeDownload()"
|
|
||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
@ -76,7 +75,7 @@
|
|||||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||||
<img
|
<img
|
||||||
v-for="(url, index) in mediaUrls"
|
v-for="(url, index) in mediaUrls"
|
||||||
:key="index"
|
:key="url"
|
||||||
:src="url"
|
:src="url"
|
||||||
class="absolute inset-0 w-full h-full object-cover"
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
v-show="index === currentImageIndex"
|
v-show="index === currentImageIndex"
|
||||||
@ -157,8 +156,8 @@
|
|||||||
<template #default>
|
<template #default>
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 class="text-base font-semibold text-zinc-100">
|
<h3 class="text-base font-semibold text-zinc-100"
|
||||||
Install {{ game.mName }}?
|
>Install {{ game.mName }}?
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="text-sm text-zinc-400">
|
<p class="text-sm text-zinc-400">
|
||||||
@ -243,10 +242,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||||
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">
|
||||||
<div class="flex-shrink-0">
|
<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" />
|
||||||
@ -259,27 +255,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div v-if="installDirs">
|
||||||
<Listbox as="div" v-model="installDir">
|
<Listbox as="div" v-model="installDir">
|
||||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||||
@ -374,7 +349,9 @@
|
|||||||
<template #buttons>
|
<template #buttons>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@click="() => install()"
|
@click="() => install()"
|
||||||
:disabled="!(versionOptions && versionOptions.length > 0)"
|
:disabled="
|
||||||
|
!(versionOptions && versionOptions.length > 0)
|
||||||
|
"
|
||||||
:loading="installLoading"
|
:loading="installLoading"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="ml-2 w-full sm:w-fit"
|
class="ml-2 w-full sm:w-fit"
|
||||||
@ -392,18 +369,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ModalTemplate>
|
</ModalTemplate>
|
||||||
|
|
||||||
<!--
|
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
|
||||||
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
|
<Transition
|
||||||
enter="transition ease-out duration-300"
|
enter="transition ease-out duration-300"
|
||||||
@ -453,7 +419,7 @@
|
|||||||
<img
|
<img
|
||||||
v-for="(url, index) in mediaUrls"
|
v-for="(url, index) in mediaUrls"
|
||||||
v-show="currentImageIndex === index"
|
v-show="currentImageIndex === index"
|
||||||
:key="index"
|
:key="url"
|
||||||
:src="url"
|
:src="url"
|
||||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||||
@ -474,6 +440,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
Listbox,
|
Listbox,
|
||||||
ListboxButton,
|
ListboxButton,
|
||||||
ListboxLabel,
|
ListboxLabel,
|
||||||
@ -501,6 +471,7 @@ const router = useRouter();
|
|||||||
const id = route.params.id.toString();
|
const id = route.params.id.toString();
|
||||||
|
|
||||||
const { game: rawGame, status } = await useGame(id);
|
const { game: rawGame, status } = await useGame(id);
|
||||||
|
console.log("status: ", status);
|
||||||
const game = ref(rawGame);
|
const game = ref(rawGame);
|
||||||
|
|
||||||
const remoteUrl: string = await invoke("gen_drop_url", {
|
const remoteUrl: string = await invoke("gen_drop_url", {
|
||||||
@ -511,10 +482,7 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
|
|||||||
|
|
||||||
// Get all available images
|
// Get all available images
|
||||||
const mediaUrls = await Promise.all(
|
const mediaUrls = await Promise.all(
|
||||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
||||||
const src = await useObject(v);
|
|
||||||
return src;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const htmlDescription = micromark(game.value.mDescription);
|
const htmlDescription = micromark(game.value.mDescription);
|
||||||
@ -534,13 +502,13 @@ async function installFlow() {
|
|||||||
installDirs.value = undefined;
|
installDirs.value = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
versionOptions.value = await invoke("fetch_game_version_options", {
|
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||||
gameId: game.value.id,
|
gameId: game.value.id,
|
||||||
});
|
});
|
||||||
|
console.log(versionOptions.value);
|
||||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installError.value = (error as string).toString();
|
installError.value = (error as string).toString();
|
||||||
versionOptions.value = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,14 +533,6 @@ async function install() {
|
|||||||
installLoading.value = false;
|
installLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeDownload() {
|
|
||||||
try {
|
|
||||||
await invoke("resume_download", { gameId: game.value.id });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launch() {
|
async function launch() {
|
||||||
try {
|
try {
|
||||||
await invoke("launch_game", { id: game.value.id });
|
await invoke("launch_game", { id: game.value.id });
|
||||||
@ -1,46 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
||||||
<div
|
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
|
||||||
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
|
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
|
||||||
>
|
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||||
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
|
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
|
||||||
<span v-if="stats.time > 0" class="text-xs text-zinc-400"
|
|
||||||
>{{ formatTime(stats.time) }} left</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
|
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||||
<div
|
<div v-for="bar in speedHistory" :style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||||
v-for="bar in speedHistory"
|
class="w-[8px] bg-blue-600/40" />
|
||||||
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
|
||||||
class="w-[3px] bg-blue-600 rounded-t-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="queue.queue" @end="onEnd">
|
<draggable v-model="queue.queue" @end="onEnd">
|
||||||
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
||||||
<li
|
<li v-if="downloads.has(element.meta)" :key="element.meta.id"
|
||||||
v-if="games[element.meta.id]"
|
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
|
||||||
:key="element.meta.id"
|
|
||||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
|
|
||||||
>
|
|
||||||
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
||||||
<img
|
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="downloads.get(element.meta)!.queueMeta.cover"
|
||||||
class="size-24 flex-none bg-zinc-800 object-cover rounded"
|
alt="" />
|
||||||
:src="games[element.meta.id].cover"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
<p class="text-xl font-semibold text-zinc-100">
|
<p class="text-xl font-semibold text-zinc-100">
|
||||||
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
||||||
<span class="absolute inset-x-0 -top-px bottom-0" />
|
<span class="absolute inset-x-0 -top-px bottom-0" />
|
||||||
{{ games[element.meta.id].game.mName }}
|
{{ downloads.get(element.meta)!.queueMeta.mName }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 flex text-xs/5 text-gray-500">
|
<p class="mt-1 flex text-xs/5 text-gray-500">
|
||||||
{{ games[element.meta.id].game.mShortDescription }}
|
{{ downloads.get(element.meta)!.queueMeta.mShortDescription }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,40 +35,28 @@
|
|||||||
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
||||||
{{ element.status }}
|
{{ element.status }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
|
||||||
v-if="element.progress"
|
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
|
||||||
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-2 bg-blue-600"
|
|
||||||
:style="{ width: `${element.progress * 100}%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"><span
|
||||||
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
class="text-zinc-300">{{
|
||||||
><span class="text-zinc-300">{{
|
formatKilobytes(element.current / 1000)
|
||||||
formatKilobytes(element.current / 1000)
|
}}</span>
|
||||||
}}B</span>
|
|
||||||
/
|
/
|
||||||
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
|
<span class="">{{ formatKilobytes(element.max / 1000) }}</span>
|
||||||
><ServerIcon class="size-5"
|
<ServerIcon class="size-5" />
|
||||||
/></span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="() => cancelGame(element.meta)" class="group">
|
<button @click="() => cancelGame(element.meta)" class="group">
|
||||||
<XMarkIcon
|
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||||
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
aria-hidden="true" />
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<p v-else>Loading...</p>
|
<p v-else>Loading...</p>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div
|
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
|
||||||
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
|
|
||||||
v-if="queue.queue.length == 0"
|
|
||||||
>
|
|
||||||
No items in the queue
|
No items in the queue
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -91,12 +65,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
import { useStatus } from "~/composables/game";
|
||||||
|
import type { DownloadableMetadata, Game, GameStatus, QueueMetadata } from "~/types";
|
||||||
// const actionNames = {
|
|
||||||
// [GameStatusEnum.Downloading]: "downloading",
|
|
||||||
// [GameStatusEnum.Verifying]: "verifying",
|
|
||||||
// }
|
|
||||||
|
|
||||||
const windowWidth = ref(window.innerWidth);
|
const windowWidth = ref(window.innerWidth);
|
||||||
window.addEventListener("resize", (event) => {
|
window.addEventListener("resize", (event) => {
|
||||||
@ -105,16 +75,14 @@ window.addEventListener("resize", (event) => {
|
|||||||
|
|
||||||
const queue = useQueueState();
|
const queue = useQueueState();
|
||||||
const stats = useStatsState();
|
const stats = useStatsState();
|
||||||
const speedHistory = useDownloadHistory();
|
const speedHistory = useState<Array<number>>(() => []);
|
||||||
const speedHistoryMax = computed(() => windowWidth.value / 4);
|
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||||
const speedMax = computed(
|
const speedMax = computed(
|
||||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
|
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
|
||||||
);
|
);
|
||||||
const previousGameId = useState<string | undefined>('previous_game');
|
const previousGameId = ref<string | undefined>();
|
||||||
|
|
||||||
const games: Ref<{
|
const downloads: Ref<Map<DownloadableMetadata, { queueMeta: QueueMetadata, status: Ref<GameStatus> }>> = ref(new Map());
|
||||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
|
||||||
}> = ref({});
|
|
||||||
|
|
||||||
function resetHistoryGraph() {
|
function resetHistoryGraph() {
|
||||||
speedHistory.value = [];
|
speedHistory.value = [];
|
||||||
@ -122,15 +90,14 @@ function resetHistoryGraph() {
|
|||||||
}
|
}
|
||||||
function checkReset(v: QueueState) {
|
function checkReset(v: QueueState) {
|
||||||
const currentGame = v.queue.at(0)?.meta.id;
|
const currentGame = v.queue.at(0)?.meta.id;
|
||||||
// If we don't have a game
|
|
||||||
if (!currentGame) return;
|
|
||||||
|
|
||||||
// If we're finished
|
// If we're finished
|
||||||
if (!currentGame && previousGameId.value) {
|
if (!currentGame && previousGameId.value) {
|
||||||
previousGameId.value = undefined;
|
previousGameId.value = undefined;
|
||||||
resetHistoryGraph();
|
resetHistoryGraph();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If we don't have a game
|
||||||
|
if (!currentGame) return;
|
||||||
// If we started a new download
|
// If we started a new download
|
||||||
if (currentGame && !previousGameId.value) {
|
if (currentGame && !previousGameId.value) {
|
||||||
previousGameId.value = currentGame;
|
previousGameId.value = currentGame;
|
||||||
@ -150,23 +117,23 @@ watch(queue, (v) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(stats, (v) => {
|
watch(stats, (v) => {
|
||||||
if(v.speed == 0) return;
|
|
||||||
const newLength = speedHistory.value.push(v.speed);
|
const newLength = speedHistory.value.push(v.speed);
|
||||||
if (newLength > speedHistoryMax.value) {
|
if (newLength > speedHistoryMax.value) {
|
||||||
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
|
speedHistory.value.splice(0, 1);
|
||||||
}
|
}
|
||||||
checkReset(queue.value);
|
checkReset(queue.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadGamesForQueue(v: typeof queue.value) {
|
function loadGamesForQueue(v: typeof queue.value) {
|
||||||
for (const {
|
for (const {
|
||||||
meta: { id },
|
meta,
|
||||||
} of v.queue) {
|
} of v.queue) {
|
||||||
if (games.value[id]) return;
|
if (downloads.value.get(meta)) return;
|
||||||
(async () => {
|
(async () => {
|
||||||
const gameData = await useGame(id);
|
const queueMeta: QueueMetadata = await invoke("get_queue_metadata", {meta});
|
||||||
const cover = await useObject(gameData.game.mCoverObjectId);
|
const status = useStatus(meta)!;
|
||||||
games.value[id] = { ...gameData, cover };
|
const cover = await useObject(queueMeta.cover);
|
||||||
|
downloads.value.set(meta, { queueMeta: { ...queueMeta, cover }, status });
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +152,7 @@ async function cancelGame(meta: DownloadableMetadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatKilobytes(bytes: number): string {
|
function formatKilobytes(bytes: number): string {
|
||||||
const units = ["K", "M", "G", "T", "P"];
|
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||||
let value = bytes;
|
let value = bytes;
|
||||||
let unitIndex = 0;
|
let unitIndex = 0;
|
||||||
const scalar = 1000;
|
const scalar = 1000;
|
||||||
@ -68,7 +68,7 @@
|
|||||||
Open Data Directory
|
Open Data Directory
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="() => openLogFile()"
|
@click="() => queue_url_download()"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 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"
|
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 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"
|
||||||
>
|
>
|
||||||
@ -106,6 +106,8 @@ const systemData = await invoke<{
|
|||||||
dataDir: string;
|
dataDir: string;
|
||||||
}>("fetch_system_data");
|
}>("fetch_system_data");
|
||||||
|
|
||||||
|
console.log(systemData);
|
||||||
|
|
||||||
clientId.value = systemData.clientId;
|
clientId.value = systemData.clientId;
|
||||||
baseUrl.value = systemData.baseUrl;
|
baseUrl.value = systemData.baseUrl;
|
||||||
dataDir.value = systemData.dataDir;
|
dataDir.value = systemData.dataDir;
|
||||||
@ -113,6 +115,15 @@ dataDir.value = systemData.dataDir;
|
|||||||
const currentPlatform = await platform();
|
const currentPlatform = await platform();
|
||||||
platformInfo.value = currentPlatform;
|
platformInfo.value = currentPlatform;
|
||||||
|
|
||||||
|
async function queue_url_download() {
|
||||||
|
try {
|
||||||
|
await invoke("queue_url_download", { url: "https://codeload.github.com/Drop-OSS/drop-app/zip/refs/heads/develop"});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDataDir() {
|
async function openDataDir() {
|
||||||
if (!dataDir.value) return;
|
if (!dataDir.value) return;
|
||||||
try {
|
try {
|
||||||
7
pages/settings/interface.vue
Normal file
7
pages/settings/interface.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
4
pages/store/index.vue
Normal file
4
pages/store/index.vue
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<iframe src="server://drop.local/store" class="w-full h-full" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts"></script>
|
||||||
588
src-tauri/Cargo.lock
generated
588
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.3"
|
version = "0.3.0-rc-2"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ tauri-build = { version = "2.0.0", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri-plugin-shell = "2.2.1"
|
tauri-plugin-shell = "2.2.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde-binary = "0.5.0"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
@ -65,17 +66,7 @@ whoami = "1.6.0"
|
|||||||
filetime = "0.2.25"
|
filetime = "0.2.25"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
known-folders = "1.2.0"
|
known-folders = "1.2.0"
|
||||||
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
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"
|
|
||||||
tokio-util = { version = "0.7.16", features = ["io"] }
|
|
||||||
futures-core = "0.3.31"
|
|
||||||
bytes = "1.10.1"
|
|
||||||
# tailscale = { path = "./tailscale" }
|
# tailscale = { path = "./tailscale" }
|
||||||
|
|
||||||
[dependencies.dynfmt]
|
[dependencies.dynfmt]
|
||||||
@ -83,8 +74,8 @@ version = "0.1.5"
|
|||||||
features = ["curly"]
|
features = ["curly"]
|
||||||
|
|
||||||
[dependencies.tauri]
|
[dependencies.tauri]
|
||||||
version = "2.7.0"
|
version = "2.1.1"
|
||||||
features = ["protocol-asset", "tray-icon"]
|
features = ["tray-icon"]
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.40.0"
|
version = "1.40.0"
|
||||||
@ -107,17 +98,9 @@ version = "2"
|
|||||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12.22"
|
version = "0.12"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = [
|
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
|
||||||
"json",
|
|
||||||
"http2",
|
|
||||||
"blocking",
|
|
||||||
"rustls-tls",
|
|
||||||
"native-tls-alpn",
|
|
||||||
"rustls-tls-native-roots",
|
|
||||||
"stream",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build();
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"os:default",
|
"os:default"
|
||||||
"opener:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
@ -17,6 +17,7 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
|
|||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.settings.autostart = enabled;
|
db_handle.settings.autostart = enabled;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,10 @@ pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mut
|
|||||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||||
match download_manager.ensure_terminated() {
|
match download_manager.ensure_terminated() {
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Ok(()) => debug!("download manager terminated correctly"),
|
Ok(_) => debug!("download manager terminated correctly"),
|
||||||
Err(()) => error!("download manager failed to terminate correctly"),
|
Err(_) => error!("download manager failed to terminate correctly"),
|
||||||
},
|
},
|
||||||
Err(e) => panic!("{e:?}"),
|
Err(e) => panic!("{:?}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
use crate::AppState;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
client::url_downloader::URLDownloader, download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable}, error::download_manager_error::DownloadManagerError, AppState
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_state(
|
pub fn fetch_state(
|
||||||
@ -9,3 +13,22 @@ pub fn fetch_state(
|
|||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn queue_url_download(
|
||||||
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
|
url: String
|
||||||
|
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||||
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
|
let game_download_agent = Arc::new(Box::new(URLDownloader::new(
|
||||||
|
String::from("Test URL Download"),
|
||||||
|
"/home/quexeky/Downloads/test_url_download",
|
||||||
|
sender,
|
||||||
|
url,
|
||||||
|
)) as Box<dyn Downloadable + Send + Sync>);
|
||||||
|
Ok(state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.download_manager
|
||||||
|
.queue_download(game_download_agent)?)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
pub mod autostart;
|
pub mod autostart;
|
||||||
pub mod cleanup;
|
pub mod cleanup;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod url_downloader;
|
||||||
173
src-tauri/src/client/url_downloader.rs
Normal file
173
src-tauri/src/client/url_downloader.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{mpsc::Sender, Arc, Mutex},
|
||||||
|
usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{debug, error};
|
||||||
|
use reqwest::redirect::Policy;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::
|
||||||
|
models::data::{DownloadType, DownloadableMetadata}
|
||||||
|
,
|
||||||
|
download_manager::{
|
||||||
|
download_manager::{DownloadManagerSignal, DownloadStatus},
|
||||||
|
downloadable::Downloadable,
|
||||||
|
util::{
|
||||||
|
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||||
|
progress_object::{ProgressHandle, ProgressObject},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
games::downloads::download_logic::{DropDownloadPipeline, DropWriter},
|
||||||
|
DB,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct URLDownloader {
|
||||||
|
id: String,
|
||||||
|
version: String,
|
||||||
|
url: String,
|
||||||
|
control_flag: DownloadThreadControl,
|
||||||
|
progress: Arc<ProgressObject>,
|
||||||
|
target: PathBuf,
|
||||||
|
sender: Sender<DownloadManagerSignal>,
|
||||||
|
status: Mutex<DownloadStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct URLDownloaderManager {
|
||||||
|
current_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl URLDownloader {
|
||||||
|
pub fn new<S: Into<String>, P: AsRef<Path>>(
|
||||||
|
id: String,
|
||||||
|
target: P,
|
||||||
|
sender: Sender<DownloadManagerSignal>,
|
||||||
|
url: S,
|
||||||
|
) -> Self {
|
||||||
|
// Don't run by default
|
||||||
|
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
version: String::new(),
|
||||||
|
control_flag,
|
||||||
|
target: target.as_ref().into(),
|
||||||
|
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||||
|
sender,
|
||||||
|
status: Mutex::new(DownloadStatus::Queued),
|
||||||
|
url: url.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download(&self, _app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
|
// TODO: Fix these unwraps and implement From<io::Error> for ApplicationDownloadError
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.redirect(Policy::default())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = client.head(&self.url).send().unwrap();
|
||||||
|
let content_length = response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_LENGTH)
|
||||||
|
.map(|x| x.to_str().unwrap().parse().unwrap())
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
let response = client.get(&self.url).send().unwrap();
|
||||||
|
|
||||||
|
self.set_progress_object_params(content_length);
|
||||||
|
|
||||||
|
let progress = self.progress.get(0);
|
||||||
|
|
||||||
|
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||||
|
|
||||||
|
let mut pipeline = DropDownloadPipeline::new(
|
||||||
|
response,
|
||||||
|
DropWriter::new(&self.target),
|
||||||
|
&self.control_flag,
|
||||||
|
progress_handle,
|
||||||
|
content_length,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let completed = pipeline
|
||||||
|
.copy()
|
||||||
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
|
if !completed {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
fn set_progress_object_params(&self, max: usize) {
|
||||||
|
// Avoid re-setting it
|
||||||
|
if self.progress.get_max() != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progress.set_max(max);
|
||||||
|
self.progress.set_size(1);
|
||||||
|
self.progress.set_time_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Downloadable for URLDownloader {
|
||||||
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
|
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||||
|
self.download(app_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(&self) -> Arc<ProgressObject> {
|
||||||
|
self.progress.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_flag(&self) -> DownloadThreadControl {
|
||||||
|
self.control_flag.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> DownloadableMetadata {
|
||||||
|
DownloadableMetadata {
|
||||||
|
id: self.id.clone(),
|
||||||
|
version: Some(self.version.clone()),
|
||||||
|
download_type: DownloadType::Tool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||||
|
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||||
|
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||||
|
app_handle
|
||||||
|
.emit("download_error", error.to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
error!("error while managing download: {}", error);
|
||||||
|
|
||||||
|
let mut handle = DB.borrow_data_mut().unwrap();
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.transient_statuses
|
||||||
|
.remove(&self.metadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_complete(&self, _app_handle: &tauri::AppHandle) {
|
||||||
|
debug!("Completed url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
||||||
|
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
|
||||||
|
debug!("Incomplete url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
||||||
|
debug!("Cancelled url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> DownloadStatus {
|
||||||
|
self.status.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user