mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 12:32:14 +10:00
Compare commits
22 Commits
v0.3.0-rc-
...
v0.3.2-dl
| Author | SHA1 | Date | |
|---|---|---|---|
| 124d51bced | |||
| 3b830e2a44 | |||
| 75a4b73ee1 | |||
| 339d707092 | |||
| 776dc8fe7a | |||
| dbe8c8df4d | |||
| 35f49b8811 | |||
| cc5339a389 | |||
| 6104bfda72 | |||
| be688cb18f | |||
| 13cc69f10e | |||
| 574782f445 | |||
| b5a8543194 | |||
| d0e4aea5ce | |||
| 739e6166c5 | |||
| 682c6e9c0b | |||
| 46e1f16cdd | |||
| d19f9bbc31 | |||
| 2913fdf35b | |||
| f9fdf151ea | |||
| 495d93705e | |||
| c477dd4872 |
23
.github/workflows/clippy.yml
vendored
Normal file
23
.github/workflows/clippy.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
on: push
|
||||
name: Clippy check
|
||||
jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install dependencies (ubuntu only)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path ./src-tauri/Cargo.toml
|
||||
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
@ -22,9 +22,9 @@ jobs:
|
||||
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.
|
||||
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||
args: ''
|
||||
- platform: 'ubuntu-24.04-arm'
|
||||
- platform: 'ubuntu-22.04-arm'
|
||||
args: '--target aarch64-unknown-linux-gnu'
|
||||
- platform: 'windows-latest'
|
||||
args: ''
|
||||
@ -48,12 +48,42 @@ jobs:
|
||||
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.
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
# 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
|
||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
@ -61,10 +91,14 @@ jobs:
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
NO_STRIP: true
|
||||
with:
|
||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'Auto-release v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: ${{ matrix.args }}
|
||||
args: ${{ matrix.args }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -26,4 +26,7 @@ dist-ssr
|
||||
.output
|
||||
|
||||
src-tauri/flamegraph.svg
|
||||
src-tauri/perf*
|
||||
src-tauri/perf*
|
||||
|
||||
/*.AppImage
|
||||
/squashfs-root
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@ -1,9 +1,6 @@
|
||||
[submodule "drop-base"]
|
||||
path = drop-base
|
||||
url = https://github.com/drop-oss/drop-base
|
||||
[submodule "src-tauri/tailscale/libtailscale"]
|
||||
path = src-tauri/tailscale/libtailscale
|
||||
url = https://github.com/tailscale/libtailscale.git
|
||||
[submodule "src-tauri/umu/umu-launcher"]
|
||||
path = src-tauri/umu/umu-launcher
|
||||
url = https://github.com/Open-Wine-Components/umu-launcher.git
|
||||
[submodule "libs/drop-base"]
|
||||
path = libs/drop-base
|
||||
url = https://github.com/drop-oss/drop-base.git
|
||||
|
||||
48
build.mjs
Normal file
48
build.mjs
Normal file
@ -0,0 +1,48 @@
|
||||
import fs from "fs";
|
||||
import process from "process";
|
||||
import childProcess from "child_process";
|
||||
import createLogger from "pino";
|
||||
|
||||
const OUTPUT = "./.output";
|
||||
const logger = createLogger({ transport: { target: "pino-pretty" } });
|
||||
|
||||
async function spawn(exec, opts) {
|
||||
const output = childProcess.spawn(exec, { ...opts, shell: true });
|
||||
output.stdout.on("data", (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
output.stderr.on("data", (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
output.on("error", (err) => reject(err));
|
||||
output.on("exit", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
const views = fs.readdirSync(".").filter((view) => {
|
||||
const expectedPath = `./${view}/package.json`;
|
||||
return fs.existsSync(expectedPath);
|
||||
});
|
||||
|
||||
fs.mkdirSync(OUTPUT, { recursive: true });
|
||||
|
||||
for (const view of views) {
|
||||
const loggerChild = logger.child({});
|
||||
process.chdir(`./${view}`);
|
||||
|
||||
loggerChild.info(`Install deps for "${view}"`);
|
||||
await spawn("yarn");
|
||||
|
||||
loggerChild.info(`Building "${view}"`);
|
||||
await spawn("yarn build", {
|
||||
env: { ...process.env, NUXT_APP_BASE_URL: `/${view}/` },
|
||||
});
|
||||
|
||||
process.chdir("..");
|
||||
|
||||
fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import type { AppState } from "~/types";
|
||||
|
||||
export const useAppState = () => useState<AppState>("state");
|
||||
Submodule drop-base deleted from 26698e5b06
1
libs/drop-base
Submodule
1
libs/drop-base
Submodule
Submodule libs/drop-base added at 04125e89be
@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<LoadingIndicator />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
@ -9,8 +10,6 @@
|
||||
import "~/composables/downloads.js";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AppStatus } from "~/types";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useAppState } from "./composables/app-state.js";
|
||||
import {
|
||||
initialNavigation,
|
||||
@ -20,18 +19,26 @@ import {
|
||||
const router = useRouter();
|
||||
|
||||
const state = useAppState();
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
}
|
||||
|
||||
router.beforeEach(async () => {
|
||||
async function fetchState() {
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
if (!state.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `App state is: ${state.value}`,
|
||||
fatal: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to parse state", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await fetchState();
|
||||
|
||||
// This is inefficient but apparently we do it lol
|
||||
router.beforeEach(async () => {
|
||||
await fetchState();
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 6.5 MiB |
@ -1,52 +1,78 @@
|
||||
<template>
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
||||
<Menu
|
||||
v-if="showDropdown"
|
||||
as="div"
|
||||
class="relative inline-block text-left grow"
|
||||
>
|
||||
<div class="h-full">
|
||||
<MenuButton :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]">
|
||||
<MenuButton
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
|
||||
>
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button @click="() => emit('options')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]">
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
<MenuItem v-if="showOptions" v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('options')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button @click="() => emit('uninstall')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]">
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
@click="() => emit('uninstall')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
@ -61,6 +87,8 @@ import {
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
ServerIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
|
||||
@ -77,7 +105,7 @@ const emit = defineEmits<{
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
(e: "options"): void;
|
||||
(e: "resume"): void
|
||||
(e: "resume"): void;
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(
|
||||
@ -87,6 +115,10 @@ const showDropdown = computed(
|
||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
||||
);
|
||||
|
||||
const showOptions = computed(
|
||||
() => props.status.type === GameStatusEnum.Installed
|
||||
);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
@ -94,6 +126,8 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Downloading]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Validating]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]:
|
||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
||||
[GameStatusEnum.Installed]:
|
||||
@ -105,42 +139,45 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Running]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.PartiallyInstalled]:
|
||||
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Install",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading",
|
||||
[GameStatusEnum.Validating]: "Validating",
|
||||
[GameStatusEnum.SetupRequired]: "Setup",
|
||||
[GameStatusEnum.Installed]: "Play",
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Stop",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume"
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume",
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Queued]: QueueListIcon,
|
||||
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Validating]: ServerIcon,
|
||||
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
||||
[GameStatusEnum.Installed]: PlayIcon,
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: PlayIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
|
||||
[GameStatusEnum.Running]: StopIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.Remote]: () => emit("install"),
|
||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||
[GameStatusEnum.Validating]: () => emit("queue"),
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => { },
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Running]: () => emit("kill"),
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
|
||||
};
|
||||
</script>
|
||||
@ -37,7 +37,7 @@
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
</li>
|
||||
<OfflineHeaderWidget v-if="state.status === AppStatus.Offline" />
|
||||
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
||||
<HeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Menu v-if="state.user" as="div" class="relative inline-block">
|
||||
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<HeaderWidget>
|
||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||
@ -23,7 +23,7 @@
|
||||
<MenuItems
|
||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||
>
|
||||
<PanelWidget class="flex-col gap-y-2">
|
||||
<div class="flex-col gap-y-2">
|
||||
<NuxtLink
|
||||
to="/id/me"
|
||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||
@ -65,7 +65,7 @@
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</PanelWidget>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
@ -87,7 +87,7 @@ router.afterEach(() => {
|
||||
|
||||
const state = useAppState();
|
||||
const profilePictureUrl: string = await useObject(
|
||||
state.value.user?.profilePictureObjectId ?? ""
|
||||
state.value?.user?.profilePictureObjectId ?? ""
|
||||
);
|
||||
const adminUrl: string = await invoke("gen_drop_url", {
|
||||
path: "/admin",
|
||||
@ -13,11 +13,7 @@
|
||||
<div class="max-w-lg">
|
||||
<slot />
|
||||
<div class="mt-10">
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
||||
>
|
||||
<div>
|
||||
<div v-if="loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@ -37,10 +33,19 @@
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span v-else>
|
||||
Sign in with your browser <span aria-hidden="true">→</span>
|
||||
<span class="inline-flex gap-x-8 items-center" v-else>
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
|
||||
>
|
||||
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</button>
|
||||
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
||||
Use a code →
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
@ -121,6 +126,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const loading = ref(false);
|
||||
@ -1,19 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="relative mb-3 transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
<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>
|
||||
<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..."
|
||||
/>
|
||||
<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">
|
||||
@ -60,7 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||
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";
|
||||
@ -70,24 +81,26 @@ import { listen } from "@tauri-apps/api/event";
|
||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "text-green-500",
|
||||
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-blue-500",
|
||||
[GameStatusEnum.Updating]: "text-blue-500",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-600"
|
||||
[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"
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
@ -101,16 +114,20 @@ 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) {
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) rawGames.value = [];
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||
for (const game of newGames) {
|
||||
if (games[game.id]) continue;
|
||||
games[game.id] = await useGame(game.id);
|
||||
}
|
||||
for (const game of rawGames.value) {
|
||||
for (const game of newGames) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
await calculateGames();
|
||||
7
main/components/LoadingIndicator.vue
Normal file
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const loading = useLoadingIndicator();
|
||||
|
||||
watch(loading.isLoading, console.log);
|
||||
</script>
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
>
|
||||
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
||||
<Wordmark class="mt-1" />
|
||||
3
main/composables/app-state.ts
Normal file
3
main/composables/app-state.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { AppState } from "~/types";
|
||||
|
||||
export const useAppState = () => useState<AppState | undefined>("state");
|
||||
@ -14,7 +14,6 @@ export type SerializedGameStatus = [
|
||||
];
|
||||
|
||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||
console.log(status);
|
||||
if (status[0]) {
|
||||
return {
|
||||
type: status[0].type,
|
||||
@ -48,7 +47,6 @@ export const useGame = async (gameId: string) => {
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
} = event.payload as any;
|
||||
console.log(payload.status);
|
||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||
|
||||
/**
|
||||
@ -18,7 +18,7 @@ export function setupHooks() {
|
||||
});
|
||||
|
||||
listen("auth/finished", async (event) => {
|
||||
router.push("/store");
|
||||
router.push("/library");
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
});
|
||||
|
||||
@ -30,12 +30,31 @@ export function setupHooks() {
|
||||
description: `Drop encountered an error while downloading your game: "${(
|
||||
event.payload as unknown as string
|
||||
).toString()}"`,
|
||||
buttonText: "Close"
|
||||
buttonText: "Close",
|
||||
},
|
||||
(e, c) => c()
|
||||
);
|
||||
});
|
||||
|
||||
// This is for errors that (we think) aren't our fault
|
||||
listen("launch_external_error", (event) => {
|
||||
createModal(
|
||||
ModalType.Confirmation,
|
||||
{
|
||||
title: "Did something go wrong?",
|
||||
description:
|
||||
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
|
||||
buttonText: "Open",
|
||||
},
|
||||
async (e, c) => {
|
||||
if (e == "confirm") {
|
||||
await invoke("open_process_logs", { gameId: event.payload });
|
||||
}
|
||||
c();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
@ -46,7 +65,13 @@ export function setupHooks() {
|
||||
*/
|
||||
}
|
||||
|
||||
export function initialNavigation(state: Ref<AppState>) {
|
||||
export function initialNavigation(state: ReturnType<typeof useAppState>) {
|
||||
if (!state.value)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "App state not valid",
|
||||
fatal: true,
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
switch (state.value.status) {
|
||||
@ -63,6 +88,6 @@ export function initialNavigation(state: Ref<AppState>) {
|
||||
router.push("/error/serverunavailable");
|
||||
break;
|
||||
default:
|
||||
router.push("/store");
|
||||
router.push("/library");
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@
|
||||
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
|
||||
>
|
||||
<Logo class="h-10 w-auto sm:h-12" />
|
||||
|
||||
</header>
|
||||
<main
|
||||
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
|
||||
@ -13,5 +13,9 @@ export default defineNuxtConfig({
|
||||
|
||||
ssr: false,
|
||||
|
||||
extends: [["./drop-base"]],
|
||||
extends: [["../libs/drop-base"]],
|
||||
|
||||
app: {
|
||||
baseURL: "/main",
|
||||
}
|
||||
});
|
||||
37
main/package.json
Normal file
37
main/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "view",
|
||||
"private": true,
|
||||
"version": "0.3.2-dl",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt generate",
|
||||
"dev": "nuxt dev",
|
||||
"postinstall": "nuxt prepare",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"koa": "^2.16.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.16.0",
|
||||
"scss": "^0.2.4",
|
||||
"vue-router": "latest",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
37
main/pages/auth/code.vue
Normal file
37
main/pages/auth/code.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="min-h-full w-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Device authorization
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-md mx-auto">
|
||||
Open Drop on another one of your devices, and use your account
|
||||
dropdown to "Authorize client", and enter the code below.
|
||||
</p>
|
||||
<div
|
||||
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
|
||||
>
|
||||
<span v-for="letter in code.split('')">{{ letter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
|
||||
><span aria-hidden="true">←</span> Use a different method
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const code = await invoke<string>("auth_initiate_code");
|
||||
|
||||
definePageMeta({
|
||||
layout: "mini",
|
||||
});
|
||||
</script>
|
||||
@ -76,7 +76,7 @@
|
||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
:key="url"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
v-show="index === currentImageIndex"
|
||||
@ -157,8 +157,8 @@
|
||||
<template #default>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<h3 class="text-base font-semibold text-zinc-100"
|
||||
>Install {{ game.mName }}?
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Install {{ game.mName }}?
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-zinc-400">
|
||||
@ -350,9 +350,7 @@
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => install()"
|
||||
:disabled="
|
||||
!(versionOptions && versionOptions.length > 0)
|
||||
"
|
||||
:disabled="!(versionOptions && versionOptions.length > 0)"
|
||||
:loading="installLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
@ -370,7 +368,18 @@
|
||||
</template>
|
||||
</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
|
||||
enter="transition ease-out duration-300"
|
||||
@ -420,7 +429,7 @@
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
v-show="currentImageIndex === index"
|
||||
:key="url"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||
@ -441,10 +450,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
@ -482,7 +487,10 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||
|
||||
// Get all available images
|
||||
const mediaUrls = await Promise.all(
|
||||
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||
const src = await useObject(v);
|
||||
return src;
|
||||
})
|
||||
);
|
||||
|
||||
const htmlDescription = micromark(game.value.mDescription);
|
||||
@ -496,7 +504,6 @@ const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
@ -535,12 +542,11 @@ async function install() {
|
||||
}
|
||||
|
||||
async function resumeDownload() {
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id })
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
@ -106,8 +106,6 @@ const systemData = await invoke<{
|
||||
dataDir: string;
|
||||
}>("fetch_system_data");
|
||||
|
||||
console.log(systemData);
|
||||
|
||||
clientId.value = systemData.clientId;
|
||||
baseUrl.value = systemData.baseUrl;
|
||||
dataDir.value = systemData.dataDir;
|
||||
37
main/pages/store/index.vue
Normal file
37
main/pages/store/index.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="grow w-full h-full flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<BuildingStorefrontIcon
|
||||
class="h-12 w-12 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Store not supported in client
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-zinc-400 max-w-lg">
|
||||
Currently, Drop requires you to view the store in your browser.
|
||||
Please click the button below to open it in your default browser.
|
||||
</p>
|
||||
<NuxtLink
|
||||
:href="storeUrl"
|
||||
target="_blank"
|
||||
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
||||
>
|
||||
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BuildingStorefrontIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
|
||||
</script>
|
||||
5
main/tsconfig.json
Normal file
5
main/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"exclude": ["src-tauri/**/*"]
|
||||
}
|
||||
@ -54,12 +54,13 @@ export enum GameStatusEnum {
|
||||
Remote = "Remote",
|
||||
Queued = "Queued",
|
||||
Downloading = "Downloading",
|
||||
Validating = "Validating",
|
||||
Installed = "Installed",
|
||||
Updating = "Updating",
|
||||
Uninstalling = "Uninstalling",
|
||||
SetupRequired = "SetupRequired",
|
||||
Running = "Running",
|
||||
PartiallyInstalled = "PartiallyInstalled"
|
||||
PartiallyInstalled = "PartiallyInstalled",
|
||||
}
|
||||
|
||||
export type GameStatus = {
|
||||
8091
main/yarn.lock
Normal file
8091
main/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
22
optimize-appimage.sh
Executable file
22
optimize-appimage.sh
Executable file
@ -0,0 +1,22 @@
|
||||
## This script is largely useless, because there's not much we can do about AppImage size
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# build tauri apps
|
||||
# NO_STRIP=true yarn tauri build -- --verbose
|
||||
|
||||
# unpack appimage
|
||||
APPIMAGE=$(ls ./src-tauri/target/release/bundle/appimage/*.AppImage)
|
||||
"$APPIMAGE" --appimage-extract
|
||||
|
||||
# strip binary
|
||||
APPIMAGE_UNPACK="./squashfs-root"
|
||||
find $APPIMAGE_UNPACK -type f -exec strip -s {} \;
|
||||
|
||||
APPIMAGETOOL=$(echo "obsolete-appimagetool-$ARCH.AppImage")
|
||||
wget -O $APPIMAGETOOL "https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGETOOL"
|
||||
chmod +x $APPIMAGETOOL
|
||||
|
||||
APPIMAGE_OUTPUT=$(./$APPIMAGETOOL $APPIMAGE_UNPACK | grep ".AppImage" | grep squashfs-root | awk '{ print $6 }')
|
||||
|
||||
mv $APPIMAGE_OUTPUT "$APPIMAGE"
|
||||
45
package.json
45
package.json
@ -1,45 +1,22 @@
|
||||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.3.0-rc-5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"build": "node ./build.mjs",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-shell": "^2.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"
|
||||
"@tauri-apps/api": "^2.7.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tauri-apps/cli": ">=2.0.0",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
"@tauri-apps/cli": "^2.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
<template>
|
||||
<iframe src="server://drop.local/store" class="w-full h-full" />
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
520
src-tauri/Cargo.lock
generated
520
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.0-rc-5"
|
||||
version = "0.3.2-dl"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -25,7 +25,6 @@ tauri-build = { version = "2.0.0", features = [] }
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde_json = "1"
|
||||
serde-binary = "0.5.0"
|
||||
rayon = "1.10.0"
|
||||
webbrowser = "1.0.2"
|
||||
url = "2.5.2"
|
||||
@ -67,6 +66,13 @@ filetime = "0.2.25"
|
||||
walkdir = "2.5.0"
|
||||
known-folders = "1.2.0"
|
||||
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
bitcode = "0.6.6"
|
||||
reqwest-websocket = "0.5.0"
|
||||
futures-lite = "2.6.0"
|
||||
page_size = "0.6.0"
|
||||
sysinfo = "0.36.1"
|
||||
humansize = "2.1.3"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
@ -74,7 +80,7 @@ version = "0.1.5"
|
||||
features = ["curly"]
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.1.1"
|
||||
version = "2.7.0"
|
||||
features = ["protocol-asset", "tray-icon"]
|
||||
|
||||
[dependencies.tokio]
|
||||
@ -100,7 +106,7 @@ features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
default-features = false
|
||||
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
|
||||
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"core:window:allow-close",
|
||||
"deep-link:default",
|
||||
"dialog:default",
|
||||
"os:default"
|
||||
"os:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
@ -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();
|
||||
match download_manager.ensure_terminated() {
|
||||
Ok(res) => match res {
|
||||
Ok(_) => debug!("download manager terminated correctly"),
|
||||
Err(_) => error!("download manager failed to terminate correctly"),
|
||||
Ok(()) => debug!("download manager terminated correctly"),
|
||||
Err(()) => error!("download manager failed to terminate correctly"),
|
||||
},
|
||||
Err(e) => panic!("{:?}", e),
|
||||
Err(e) => panic!("{e:?}"),
|
||||
}
|
||||
|
||||
app.exit(0);
|
||||
|
||||
@ -7,7 +7,7 @@ use std::{
|
||||
use serde_json::Value;
|
||||
|
||||
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::{
|
||||
@ -59,6 +59,8 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -82,7 +84,7 @@ pub fn fetch_system_data() -> SystemData {
|
||||
SystemData::new(
|
||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
db_handle.base_url.clone(),
|
||||
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
|
||||
DATA_ROOT_DIR.to_string_lossy().to_string(),
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,22 +3,22 @@ use std::{
|
||||
mem::ManuallyDrop,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error, info, warn};
|
||||
use native_model::{Decode, Encode};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use url::Url;
|
||||
|
||||
use crate::DB;
|
||||
|
||||
use super::models::data::Database;
|
||||
|
||||
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
|
||||
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
|
||||
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop")));
|
||||
|
||||
// Custom JSON serializer to support everything we need
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@ -52,15 +52,14 @@ pub trait DatabaseImpls {
|
||||
}
|
||||
impl DatabaseImpls for DatabaseInterface {
|
||||
fn set_up_database() -> DatabaseInterface {
|
||||
let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
|
||||
let db_path = data_root_dir.join("drop.db");
|
||||
let games_base_dir = data_root_dir.join("games");
|
||||
let logs_root_dir = data_root_dir.join("logs");
|
||||
let cache_dir = data_root_dir.join("cache");
|
||||
let pfx_dir = data_root_dir.join("pfx");
|
||||
let db_path = DATA_ROOT_DIR.join("drop.db");
|
||||
let games_base_dir = DATA_ROOT_DIR.join("games");
|
||||
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
||||
let cache_dir = DATA_ROOT_DIR.join("cache");
|
||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
||||
|
||||
debug!("creating data directory at {:?}", data_root_dir);
|
||||
create_dir_all(data_root_dir.clone()).unwrap();
|
||||
debug!("creating data directory at {DATA_ROOT_DIR:?}");
|
||||
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap();
|
||||
create_dir_all(&games_base_dir).unwrap();
|
||||
create_dir_all(&logs_root_dir).unwrap();
|
||||
create_dir_all(&cache_dir).unwrap();
|
||||
@ -68,20 +67,18 @@ impl DatabaseImpls for DatabaseInterface {
|
||||
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
match exists {
|
||||
true => match PathDatabase::load_from_path(db_path.clone()) {
|
||||
if exists {
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,17 +99,14 @@ fn handle_invalid_database(
|
||||
games_base_dir: PathBuf,
|
||||
cache_dir: PathBuf,
|
||||
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
warn!("{}", _e);
|
||||
warn!("{_e}");
|
||||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
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
|
||||
};
|
||||
info!(
|
||||
"old database stored at: {}",
|
||||
new_path.to_string_lossy().to_string()
|
||||
);
|
||||
info!("old database stored at: {}", new_path.to_string_lossy());
|
||||
fs::rename(&db_path, &new_path).unwrap();
|
||||
|
||||
let db = Database::new(
|
||||
@ -128,14 +122,7 @@ fn handle_invalid_database(
|
||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||
impl<'a> Deref for DBWrite<'a> {
|
||||
type Target = RwLockWriteGuard<'a, Database>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = RwLockReadGuard<'a, Database>;
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@ -146,27 +133,35 @@ impl<'a> DerefMut for DBWrite<'a> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Drop for DBWrite<'a> {
|
||||
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(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
error!("database failed to save with error {}", e);
|
||||
panic!("database failed to save with error {}", 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() {
|
||||
Ok(data) => DBRead(data),
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {}", e);
|
||||
panic!("database borrow failed with error {}", e);
|
||||
error!("database borrow failed with error {e}");
|
||||
panic!("database borrow failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,8 +170,8 @@ pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {}", e);
|
||||
panic!("database borrow mut failed with error {}", e);
|
||||
error!("database borrow mut failed with error {e}");
|
||||
panic!("database borrow mut failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,3 +2,4 @@ pub mod commands;
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod models;
|
||||
pub mod scan;
|
||||
@ -1,9 +1,15 @@
|
||||
use crate::database::models::data::Database;
|
||||
|
||||
/**
|
||||
* NEXT BREAKING CHANGE
|
||||
*
|
||||
* UPDATE DATABASE TO USE RPMSERDENAMED
|
||||
*
|
||||
* WE CAN'T DELETE ANY FIELDS
|
||||
*/
|
||||
pub mod data {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use native_model::{native_model, Model};
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type GameVersion = v1::GameVersion;
|
||||
@ -18,16 +24,14 @@ pub mod data {
|
||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod v1 {
|
||||
use crate::process::process_manager::Platform;
|
||||
use serde_with::serde_as;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use super::*;
|
||||
use super::{Deserialize, Serialize, native_model};
|
||||
|
||||
fn default_template() -> String {
|
||||
"{}".to_owned()
|
||||
@ -115,6 +119,7 @@ pub mod data {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
Updating { version_name: String },
|
||||
Validating { version_name: String },
|
||||
Running {},
|
||||
}
|
||||
|
||||
@ -134,7 +139,7 @@ pub mod data {
|
||||
pub enum DownloadType {
|
||||
Game,
|
||||
Tool,
|
||||
DLC,
|
||||
Dlc,
|
||||
Mod,
|
||||
}
|
||||
|
||||
@ -174,7 +179,10 @@ pub mod data {
|
||||
|
||||
use serde_with::serde_as;
|
||||
|
||||
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)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
@ -206,7 +214,7 @@ pub mod data {
|
||||
applications: value.applications,
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,7 +291,10 @@ pub mod data {
|
||||
mod v3 {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
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 {
|
||||
@ -297,6 +308,7 @@ pub mod data {
|
||||
pub cache_dir: PathBuf,
|
||||
pub compat_info: Option<DatabaseCompatInfo>,
|
||||
}
|
||||
|
||||
impl From<v2::Database> for Database {
|
||||
fn from(value: v2::Database) -> Self {
|
||||
Self {
|
||||
@ -306,22 +318,13 @@ pub mod data {
|
||||
applications: value.applications.into(),
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
@ -336,11 +339,11 @@ pub mod data {
|
||||
transient_statuses: HashMap::new(),
|
||||
},
|
||||
prev_database,
|
||||
base_url: "".to_owned(),
|
||||
base_url: String::new(),
|
||||
auth: None,
|
||||
settings: Settings::default(),
|
||||
cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
src-tauri/src/database/scan.rs
Normal file
52
src-tauri/src/database/scan.rs
Normal file
@ -0,0 +1,52 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,12 @@ use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.pause_downloads()
|
||||
state.lock().unwrap().download_manager.pause_downloads();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.resume_downloads()
|
||||
state.lock().unwrap().download_manager.resume_downloads();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -22,10 +22,10 @@ pub fn move_download_in_queue(
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.rearrange(old_index, new_index)
|
||||
.rearrange(old_index, new_index);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta)
|
||||
state.lock().unwrap().download_manager.cancel(meta);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex,
|
||||
mpsc::{Receiver, Sender, channel},
|
||||
},
|
||||
thread::{spawn, JoinHandle},
|
||||
thread::{JoinHandle, spawn},
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
@ -17,7 +17,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||
download_manager_frontend::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||
downloadable::Downloadable,
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
@ -176,15 +176,14 @@ impl DownloadManagerBuilder {
|
||||
DownloadManagerSignal::Cancel(meta) => {
|
||||
self.manage_cancel_signal(&meta);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
debug!("got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
debug!("queue metadata: {:?}", meta);
|
||||
debug!("queue metadata: {meta:?}");
|
||||
|
||||
if self.download_queue.exists(meta.clone()) {
|
||||
warn!("download with same ID already exists");
|
||||
@ -210,16 +209,15 @@ impl DownloadManagerBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current_download_agent.is_some() {
|
||||
if self.download_queue.read().front().unwrap()
|
||||
if self.current_download_agent.is_some()
|
||||
&& self.download_queue.read().front().unwrap()
|
||||
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||
{
|
||||
debug!(
|
||||
"Current download agent: {:?}",
|
||||
self.current_download_agent.as_ref().unwrap().metadata()
|
||||
);
|
||||
return;
|
||||
}
|
||||
{
|
||||
debug!(
|
||||
"Current download agent: {:?}",
|
||||
self.current_download_agent.as_ref().unwrap().metadata()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
@ -227,7 +225,7 @@ impl DownloadManagerBuilder {
|
||||
// Should always be Some if the above two statements keep going
|
||||
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||
|
||||
info!("starting download for {:?}", agent_data);
|
||||
info!("starting download for {agent_data:?}");
|
||||
|
||||
let download_agent = self
|
||||
.download_agent_registry
|
||||
@ -244,43 +242,47 @@ impl DownloadManagerBuilder {
|
||||
let app_handle = self.app_handle.clone();
|
||||
|
||||
*download_thread_lock = Some(spawn(move || {
|
||||
match download_agent.download(&app_handle) {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(true) => {
|
||||
debug!("download {:?} has completed", download_agent.metadata());
|
||||
match download_agent.validate() {
|
||||
Ok(true) => {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
}
|
||||
Ok(false) => {
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"download {:?} has validation error {}",
|
||||
download_agent.metadata(),
|
||||
&e
|
||||
);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
loop {
|
||||
let download_result = match download_agent.download(&app_handle) {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// If the download gets cancelled
|
||||
// immediately return, on_cancelled gets called for us earlier
|
||||
if !download_result {
|
||||
return;
|
||||
}
|
||||
// Ok(false) is for incomplete but exited properly
|
||||
Ok(false) => {
|
||||
debug!("Donwload agent finished incomplete");
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
|
||||
let validate_result = match download_agent.validate(&app_handle) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"download {:?} has validation error {}",
|
||||
download_agent.metadata(),
|
||||
&e
|
||||
);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if validate_result {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}));
|
||||
|
||||
self.set_status(DownloadManagerStatus::Downloading);
|
||||
@ -297,11 +299,12 @@ impl DownloadManagerBuilder {
|
||||
}
|
||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
debug!("got signal Completed");
|
||||
if let Some(interface) = &self.current_download_agent {
|
||||
if interface.metadata() == meta {
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
if let Some(interface) = &self.current_download_agent
|
||||
&& interface.metadata() == meta
|
||||
{
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
|
||||
self.push_ui_queue_update();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
@ -313,7 +316,7 @@ impl DownloadManagerBuilder {
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
}
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
self.set_status(DownloadManagerStatus::Error);
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
debug!("got signal Cancel");
|
||||
|
||||
@ -23,13 +23,13 @@ use super::{
|
||||
};
|
||||
|
||||
pub enum DownloadManagerSignal {
|
||||
/// Resumes (or starts) the DownloadManager
|
||||
/// Resumes (or starts) the `DownloadManager`
|
||||
Go,
|
||||
/// Pauses the DownloadManager
|
||||
/// Pauses the `DownloadManager`
|
||||
Stop,
|
||||
/// Called when a DownloadAgent has fully completed a download.
|
||||
/// Called when a `DownloadAgent` has fully completed a download.
|
||||
Completed(DownloadableMetadata),
|
||||
/// Generates and appends a DownloadAgent
|
||||
/// Generates and appends a `DownloadAgent`
|
||||
/// to the registry and queue
|
||||
Queue(DownloadAgent),
|
||||
/// Tells the Manager to stop the current
|
||||
@ -38,16 +38,11 @@ pub enum DownloadManagerSignal {
|
||||
Finish,
|
||||
/// Stops, removes, and tells a download to cleanup
|
||||
Cancel(DownloadableMetadata),
|
||||
/// Removes a given application
|
||||
Remove(DownloadableMetadata),
|
||||
/// Any error which occurs in the agent
|
||||
Error(ApplicationDownloadError),
|
||||
/// Pushes UI update
|
||||
UpdateUIQueue,
|
||||
UpdateUIStats(usize, usize), //kb/s and seconds
|
||||
/// Uninstall download
|
||||
/// Takes download ID
|
||||
Uninstall(DownloadableMetadata),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -55,8 +50,7 @@ pub enum DownloadManagerStatus {
|
||||
Downloading,
|
||||
Paused,
|
||||
Empty,
|
||||
Error(ApplicationDownloadError),
|
||||
Finished,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Serialize for DownloadManagerStatus {
|
||||
@ -64,7 +58,7 @@ impl Serialize for DownloadManagerStatus {
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!["{:?}", self])
|
||||
serializer.serialize_str(&format!["{self:?}"])
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,14 +70,14 @@ pub enum DownloadStatus {
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Accessible front-end for the DownloadManager
|
||||
/// Accessible front-end for the `DownloadManager`
|
||||
///
|
||||
/// The system works entirely through signals, both internally and externally,
|
||||
/// all of which are accessible through the DownloadManagerSignal type, but
|
||||
/// all of which are accessible through the `DownloadManagerSignal` type, but
|
||||
/// should not be used directly. Rather, signals are abstracted through this
|
||||
/// interface.
|
||||
///
|
||||
/// The actual download queue may be accessed through the .edit() function,
|
||||
/// The actual download queue may be accessed through the .`edit()` function,
|
||||
/// which provides raw access to the underlying queue.
|
||||
/// THIS EDITING IS BLOCKING!!!
|
||||
pub struct DownloadManager {
|
||||
@ -145,7 +139,7 @@ impl DownloadManager {
|
||||
pub fn rearrange(&self, current_index: usize, new_index: usize) {
|
||||
if current_index == new_index {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let needs_pause = current_index == 0 || new_index == 0;
|
||||
if needs_pause {
|
||||
@ -154,10 +148,7 @@ impl DownloadManager {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
debug!(
|
||||
"moving download at index {} to index {}",
|
||||
current_index, new_index
|
||||
);
|
||||
debug!("moving download at index {current_index} to index {new_index}");
|
||||
|
||||
let mut queue = self.edit();
|
||||
let to_move = queue.remove(current_index).unwrap();
|
||||
@ -187,17 +178,12 @@ impl DownloadManager {
|
||||
let terminator = self.terminator.lock().unwrap().take();
|
||||
terminator.unwrap().join()
|
||||
}
|
||||
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Uninstall(meta))
|
||||
.unwrap();
|
||||
}
|
||||
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
||||
self.command_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes in the locked value from .edit() and attempts to
|
||||
/// Takes in the locked value from .`edit()` and attempts to
|
||||
/// get the index of whatever id is passed in
|
||||
fn get_index_from_id(
|
||||
queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user