Compare commits

..

1 Commits

Author SHA1 Message Date
6f75bedcae chore: Add test.yml workflow
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-06 09:37:58 +10:00
154 changed files with 10068 additions and 12065 deletions

View File

@ -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

View File

@ -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.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
if: matrix.platform == 'macos-latest'
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported. Using identity: $CERT_ID"
- name: install frontend dependencies - 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,14 +61,10 @@ 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__'
releaseBody: 'See the assets to download this version and install. This release was created automatically.' releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
args: ${{ matrix.args }} args: ${{ matrix.args }}

69
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: 'test'
on:
workflow_dispatch: {}
# This can be used to automatically publish nightlies at UTC nighttime
# schedule:
# - cron: "0 2 * * *" # run at 2 AM UTC
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest' # for Arm based macs (M1 and above).
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel based macs.
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-24.04-arm'
args: '--target aarch64-unknown-linux-gnu'
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
# 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: install frontend dependencies
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: 'Auto testing release'
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}

5
.gitignore vendored
View File

@ -26,7 +26,4 @@ dist-ssr
.output .output
src-tauri/flamegraph.svg src-tauri/flamegraph.svg
src-tauri/perf* src-tauri/perf*
/*.AppImage
/squashfs-root

9
.gitmodules vendored
View File

@ -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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -1,48 +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 views = fs.readdirSync(".").filter((view) => {
const expectedPath = `./${view}/package.json`;
return fs.existsSync(expectedPath);
});
fs.mkdirSync(OUTPUT, { recursive: true });
for (const view of views) {
const loggerChild = logger.child({});
process.chdir(`./${view}`);
loggerChild.info(`Install deps for "${view}"`);
await spawn("yarn");
loggerChild.info(`Building "${view}"`);
await spawn("yarn build", {
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
});
process.chdir("..");
fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, {
recursive: true,
});
}

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,5 @@
<template> <template>
<Menu v-if="state?.user" as="div" class="relative inline-block"> <Menu v-if="state.user" as="div" class="relative inline-block">
<MenuButton> <MenuButton>
<HeaderWidget> <HeaderWidget>
<div class="inline-flex items-center text-zinc-300 hover:text-white"> <div class="inline-flex items-center text-zinc-300 hover:text-white">
@ -23,7 +23,7 @@
<MenuItems <MenuItems
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md" class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
> >
<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"
@ -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",

View File

@ -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">&rarr;</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 &rarr;
</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);

View 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>

View File

@ -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
View File

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

View File

@ -14,6 +14,7 @@ export type SerializedGameStatus = [
]; ];
export const parseStatus = (status: SerializedGameStatus): GameStatus => { export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) { if (status[0]) {
return { return {
type: status[0].type, type: status[0].type,
@ -43,11 +44,11 @@ export const useGame = async (gameId: string) => {
gameStatusRegistry[gameId] = ref(parseStatus(data.status)); gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => { listen(`update_game/${gameId}`, (event) => {
console.log(event);
const payload: { const payload: {
status: SerializedGameStatus; status: SerializedGameStatus;
version?: GameVersion; version?: GameVersion;
} = event.payload as any; } = event.payload as any;
console.log(payload.status);
gameStatusRegistry[gameId].value = parseStatus(payload.status); gameStatusRegistry[gameId].value = parseStatus(payload.status);
/** /**

View File

@ -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

Submodule drop-base added at 26698e5b06

View File

@ -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

View File

@ -1,198 +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)"
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100">
<ArrowPathIcon class="size-4" />
</button>
</div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
navIndex === currentNavigation
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]" :href="nav.route">
<div class="flex items-center w-full gap-x-3">
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
:src="icons[nav.id]" alt="" />
</div>
<div class="flex flex-col flex-1">
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
{{ nav.label }}
</p>
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
{{ gameStatusText[games[nav.id].status.value.type] }}
</p>
</div>
</div>
</NuxtLink>
</TransitionGroup>
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
<div role="status">
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
</svg>
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
import { TransitionGroup } from "vue";
import { listen } from "@tauri-apps/api/event";
// Style information
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "text-green-500",
[GameStatusEnum.Downloading]: "text-zinc-400",
[GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-500",
[GameStatusEnum.Queued]: "text-zinc-400",
[GameStatusEnum.Updating]: "text-zinc-400",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading...",
[GameStatusEnum.Validating]: "Validating...",
[GameStatusEnum.Installed]: "Installed",
[GameStatusEnum.Updating]: "Updating...",
[GameStatusEnum.Uninstalling]: "Uninstalling...",
[GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running",
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
};
const router = useRouter();
const searchQuery = ref("");
const loading = ref(false);
const games: {
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
} = {};
const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([]);
async function calculateGames(clearAll = false) {
if (clearAll) {
rawGames.value = [];
loading.value = true;
}
// If we update immediately, the navigation gets re-rendered before we
// add all the necessary state, and it freaks tf out
const newGames = await invoke<typeof rawGames.value>("fetch_library");
for (const game of newGames) {
if (games[game.id]) continue;
games[game.id] = await useGame(game.id);
}
for (const game of newGames) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId);
}
loading.value = false;
rawGames.value = newGames;
}
// Wait up to 300 ms for the library to load, otherwise
// show the loading state while we while
await new Promise<void>((r) => {
let hasResolved = false;
const resolveFunc = () => {
if (!hasResolved) r();
hasResolved = true
}
calculateGames(true).then(resolveFunc);
setTimeout(resolveFunc, 300);
})
const navigation = computed(() =>
rawGames.value.map((game) => {
const status = games[game.id].status;
const isInstalled = computed(
() =>
status.value.type != GameStatusEnum.Remote
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
id: game.id,
};
return item;
})
);
const route = useRoute();
const currentNavigation = computed(() => {
return navigation.value.findIndex((e) => e.route == route.path)
});
const filteredNavigation = computed(() => {
if (!searchQuery.value)
return navigation.value.map((e, i) => ({ ...e, index: i }));
const query = searchQuery.value.toLowerCase();
return navigation.value
.filter((nav) => nav.label.toLowerCase().includes(query))
.map((e, i) => ({ ...e, index: i }));
});
listen("update_library", async (event) => {
console.log("Updating library");
let oldNavigation = navigation.value[currentNavigation.value];
await calculateGames();
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
router.push("/library");
}
});
</script>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-active {
position: absolute;
}
</style>

View File

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

View File

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

View File

@ -1,37 +0,0 @@
{
"name": "view",
"private": true,
"version": "0.3.2",
"type": "module",
"scripts": {
"build": "nuxt generate",
"dev": "nuxt dev",
"postinstall": "nuxt prepare",
"tauri": "tauri"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": "^2.7.0",
"koa": "^2.16.1",
"markdown-it": "^14.1.0",
"micromark": "^4.0.1",
"nuxt": "^3.16.0",
"scss": "^0.2.4",
"vue-router": "latest",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -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">&larr;</span> Use a different method
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const code = await invoke<string>("auth_initiate_code");
definePageMeta({
layout: "mini",
});
</script>

View File

@ -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>

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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"

View File

@ -1,22 +1,45 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.3.0-rc-3",
"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"
} }

View File

@ -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,
@ -511,10 +481,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 +501,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 +532,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 });

View File

@ -91,12 +91,7 @@
<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 { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types"; import type { DownloadableMetadata, Game, GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => { window.addEventListener("resize", (event) => {

View File

@ -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;

4
pages/store/index.vue Normal file
View File

@ -0,0 +1,4 @@
<template>
<iframe src="server://drop.local/store" class="w-full h-full" />
</template>
<script setup lang="ts"></script>

569
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.3.2" version = "0.3.0-rc-3"
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"
@ -66,13 +67,6 @@ 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.1", features = ["rmp_serde_1_3"] } native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
page_size = "0.6.0"
sysinfo = "0.36.1"
humansize = "2.1.3"
# tailscale = { path = "./tailscale" } # tailscale = { path = "./tailscale" }
[dependencies.dynfmt] [dependencies.dynfmt]
@ -80,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"
@ -104,9 +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 = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"] features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"

View File

@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build(); tauri_build::build()
} }

View File

@ -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"
] ]
} }

View File

@ -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(())
} }

View File

@ -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);

View File

@ -1,3 +1,3 @@
pub mod autostart; pub mod autostart;
pub mod cleanup; pub mod cleanup;
pub mod commands; pub mod commands;

View File

@ -6,12 +6,10 @@ use std::{
use serde_json::Value; use serde_json::Value;
use crate::{ use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
};
use super::{ use super::{
db::{borrow_db_checked, DATA_ROOT_DIR}, db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings, models::data::Settings,
}; };
@ -28,6 +26,8 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
pub fn delete_download_dir(index: usize) { pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked(); let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index); lock.applications.install_dirs.remove(index);
drop(lock);
save_db();
} }
#[tauri::command] #[tauri::command]
@ -58,8 +58,7 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
} }
lock.applications.install_dirs.push(new_dir); lock.applications.install_dirs.push(new_dir);
drop(lock); drop(lock);
save_db();
scan_install_dirs();
Ok(()) Ok(())
} }
@ -73,6 +72,8 @@ pub fn update_settings(new_settings: Value) {
} }
let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings; db_lock.settings = new_settings;
drop(db_lock);
save_db();
} }
#[tauri::command] #[tauri::command]
pub fn fetch_settings() -> Settings { pub fn fetch_settings() -> Settings {
@ -84,7 +85,7 @@ pub fn fetch_system_data() -> SystemData {
SystemData::new( SystemData::new(
db_handle.auth.as_ref().unwrap().client_id.clone(), db_handle.auth.as_ref().unwrap().client_id.clone(),
db_handle.base_url.clone(), db_handle.base_url.clone(),
DATA_ROOT_DIR.to_string_lossy().to_string(), DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
) )
} }

View File

@ -1,24 +1,23 @@
use std::{ use std::{
fs::{self, create_dir_all}, fs::{self, create_dir_all},
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::PathBuf, path::PathBuf,
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard}, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
use chrono::Utc; use chrono::Utc;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use native_model::{Decode, Encode}; use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned}; use serde::{de::DeserializeOwned, Serialize};
use url::Url; use url::Url;
use crate::DB; use crate::DB;
use super::models::data::Database; use super::models::data::Database;
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop"))); LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
// Custom JSON serializer to support everything we need // Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -28,16 +27,15 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer for DropDatabaseSerializer
{ {
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::rmp_serde_1_3::RmpSerde::encode(val) native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
.map_err(|e| DeSerError::Internal(e.to_string()))
} }
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> { fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new(); let mut buf = Vec::new();
s.read_to_end(&mut buf) s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?; .map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf) let val =
.map_err(|e| DeSerError::Internal(e.to_string()))?; native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val) Ok(val)
} }
} }
@ -52,14 +50,15 @@ pub trait DatabaseImpls {
} }
impl DatabaseImpls for DatabaseInterface { impl DatabaseImpls for DatabaseInterface {
fn set_up_database() -> DatabaseInterface { fn set_up_database() -> DatabaseInterface {
let db_path = DATA_ROOT_DIR.join("drop.db"); let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
let games_base_dir = DATA_ROOT_DIR.join("games"); let db_path = data_root_dir.join("drop.db");
let logs_root_dir = DATA_ROOT_DIR.join("logs"); let games_base_dir = data_root_dir.join("games");
let cache_dir = DATA_ROOT_DIR.join("cache"); let logs_root_dir = data_root_dir.join("logs");
let pfx_dir = DATA_ROOT_DIR.join("pfx"); let cache_dir = data_root_dir.join("cache");
let pfx_dir = data_root_dir.join("pfx");
debug!("creating data directory at {DATA_ROOT_DIR:?}"); debug!("creating data directory at {:?}", data_root_dir);
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap(); create_dir_all(data_root_dir.clone()).unwrap();
create_dir_all(&games_base_dir).unwrap(); create_dir_all(&games_base_dir).unwrap();
create_dir_all(&logs_root_dir).unwrap(); create_dir_all(&logs_root_dir).unwrap();
create_dir_all(&cache_dir).unwrap(); create_dir_all(&cache_dir).unwrap();
@ -67,18 +66,20 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap();
if exists { match exists {
match PathDatabase::load_from_path(db_path.clone()) { true => match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db, Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir), Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
},
false => {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
} }
} else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
} }
} }
@ -99,14 +100,17 @@ fn handle_invalid_database(
games_base_dir: PathBuf, games_base_dir: PathBuf,
cache_dir: PathBuf, cache_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> { ) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
warn!("{_e}"); warn!("{}", _e);
let new_path = { let new_path = {
let time = Utc::now().timestamp(); let time = Utc::now().timestamp();
let mut base = db_path.clone(); let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{time}")); base.set_file_name(format!("drop.db.backup-{}", time));
base base
}; };
info!("old database stored at: {}", new_path.to_string_lossy()); info!(
"old database stored at: {}",
new_path.to_string_lossy().to_string()
);
fs::rename(&db_path, &new_path).unwrap(); fs::rename(&db_path, &new_path).unwrap();
let db = Database::new( let db = Database::new(
@ -118,60 +122,32 @@ fn handle_invalid_database(
PathDatabase::create_at_path(db_path, db).expect("Database could not be created") PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
} }
// To automatically save the database upon drop pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DBWrite<'_> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(()) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() { match DB.borrow_data() {
Ok(data) => DBRead(data), Ok(data) => data,
Err(e) => { Err(e) => {
error!("database borrow failed with error {e}"); error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {e}"); panic!("database borrow failed with error {}", e);
} }
} }
} }
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> { pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
match DB.borrow_data_mut() { match DB.borrow_data_mut() {
Ok(data) => DBWrite(ManuallyDrop::new(data)), Ok(data) => data,
Err(e) => { Err(e) => {
error!("database borrow mut failed with error {e}"); error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {e}"); panic!("database borrow mut failed with error {}", e);
}
}
}
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
} }
} }
} }

View File

@ -2,4 +2,3 @@ pub mod commands;
pub mod db; pub mod db;
pub mod debug; pub mod debug;
pub mod models; pub mod models;
pub mod scan;

View File

@ -1,37 +1,25 @@
/**
* NEXT BREAKING CHANGE
*
* UPDATE DATABASE TO USE RPMSERDENAMED
*
* WE CAN'T DELETE ANY FIELDS
*/
pub mod data { pub mod data {
use std::path::PathBuf; use native_model::{native_model, Model};
use native_model::native_model;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion; pub type GameVersion = v1::GameVersion;
pub type Database = v3::Database; pub type Database = v2::Database;
pub type Settings = v1::Settings; pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth; pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v2::GameDownloadStatus; pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus; pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata; pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType; pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v2::DatabaseApplications; pub type DatabaseApplications = v1::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo; pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap;
pub mod v1 { pub mod v1 {
use crate::process::process_manager::Platform; use crate::process::process_manager::Platform;
use serde_with::serde_as; use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use super::{Deserialize, Serialize, native_model}; use super::*;
fn default_template() -> String { fn default_template() -> String {
"{}".to_owned() "{}".to_owned()
@ -114,12 +102,11 @@ pub mod data {
} }
// Stuff that shouldn't be synced to disk // Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize)]
pub enum ApplicationTransientStatus { pub enum ApplicationTransientStatus {
Downloading { version_name: String }, Downloading { version_name: String },
Uninstalling {}, Uninstalling {},
Updating { version_name: String }, Updating { version_name: String },
Validating { version_name: String },
Running {}, Running {},
} }
@ -139,7 +126,7 @@ pub mod data {
pub enum DownloadType { pub enum DownloadType {
Game, Game,
Tool, Tool,
Dlc, DLC,
Mod, Mod,
} }
@ -175,129 +162,14 @@ pub mod data {
} }
pub mod v2 { pub mod v2 {
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf, process::Command};
use serde_with::serde_as; use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use super::{ use super::*;
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
GameVersion, Serialize, Settings, native_model, v1,
};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
pub umu_installed: bool,
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: None,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::path::PathBuf;
use super::{
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
Settings, native_model, v2,
};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database { pub struct Database {
#[serde(default)] #[serde(default)]
pub settings: Settings, pub settings: Settings,
@ -310,42 +182,59 @@ pub mod data {
pub compat_info: Option<DatabaseCompatInfo>, pub compat_info: Option<DatabaseCompatInfo>,
} }
impl From<v2::Database> for Database { #[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
fn from(value: v2::Database) -> Self { #[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
umu_installed: bool,
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self { Self {
settings: value.settings, settings: value.settings,
auth: value.auth, auth: value.auth,
base_url: value.base_url, base_url: value.base_url,
applications: value.applications.into(), applications: value.applications,
prev_database: value.prev_database, prev_database: value.prev_database,
cache_dir: value.cache_dir, cache_dir: value.cache_dir,
compat_info: None, compat_info: Database::create_new_compat_info(),
} }
} }
} }
} }
impl Database {
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: String::new(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: None,
}
}
}
} }

View File

@ -1,52 +0,0 @@
use std::fs;
use log::warn;
use crate::{
database::{
db::borrow_db_mut_checked,
models::data::v1::{DownloadType, DownloadableMetadata},
},
games::{
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
library::set_partially_installed_db,
},
};
pub fn scan_install_dirs() {
let mut db_lock = borrow_db_mut_checked();
for install_dir in db_lock.applications.install_dirs.clone() {
let Ok(files) = fs::read_dir(install_dir) else {
continue;
};
for game in files.into_iter().flatten() {
let drop_data_file = game.path().join(DROP_DATA_PATH);
if !drop_data_file.exists() {
continue;
}
let game_id = game.file_name().into_string().unwrap();
let Ok(drop_data) = DropData::read(&game.path()) else {
warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted?",
game.file_name().into_string().unwrap()
);
continue;
};
if db_lock.applications.game_statuses.contains_key(&game_id) {
continue;
}
let metadata = DownloadableMetadata::new(
drop_data.game_id,
Some(drop_data.game_version),
DownloadType::Game,
);
set_partially_installed_db(
&mut db_lock,
&metadata,
drop_data.base_path.to_str().unwrap().to_string(),
None,
);
}
}
}

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