mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 00:02:41 +10:00
Compare commits
2 Commits
42-feature
...
49-bug-bro
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f75bedcae | |||
| 86bce1c68d |
69
.github/workflows/test.yml
vendored
Normal file
69
.github/workflows/test.yml
vendored
Normal 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 }}
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { type Game, type GameStatus as DownloadStatus, type GameStatusEnum as DownloadStatusEnum, type GameVersion, type DownloadableMetadata, DownloadableType } from "~/types";
|
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
|
||||||
|
|
||||||
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
|
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
|
||||||
{};
|
{};
|
||||||
|
|
||||||
const downloadStatusRegistry: Map<DownloadableMetadata, Ref<DownloadStatus>> = new Map();
|
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
|
||||||
|
|
||||||
type OptionDownloadStatus = { [key in DownloadStatusEnum]: { version_name?: string } };
|
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
|
||||||
export type SerializedDownloadStatus = [
|
export type SerializedGameStatus = [
|
||||||
{ type: DownloadStatusEnum },
|
{ type: GameStatusEnum },
|
||||||
OptionDownloadStatus | null
|
OptionGameStatus | null
|
||||||
];
|
];
|
||||||
|
|
||||||
export const parseStatus = (status: SerializedDownloadStatus): DownloadStatus => {
|
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||||
console.log(status);
|
console.log(status);
|
||||||
if (status[0]) {
|
if (status[0]) {
|
||||||
return {
|
return {
|
||||||
@ -22,7 +22,7 @@ export const parseStatus = (status: SerializedDownloadStatus): DownloadStatus =>
|
|||||||
} else if (status[1]) {
|
} else if (status[1]) {
|
||||||
const [[gameStatus, options]] = Object.entries(status[1]);
|
const [[gameStatus, options]] = Object.entries(status[1]);
|
||||||
return {
|
return {
|
||||||
type: gameStatus as DownloadStatusEnum,
|
type: gameStatus as GameStatusEnum,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -30,54 +30,43 @@ export const parseStatus = (status: SerializedDownloadStatus): DownloadStatus =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useStatus = (meta: DownloadableMetadata) => {
|
|
||||||
return downloadStatusRegistry.get(meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGame = async (gameId: string) => {
|
export const useGame = async (gameId: string) => {
|
||||||
const data: {
|
|
||||||
game: Game;
|
|
||||||
status: SerializedDownloadStatus;
|
|
||||||
version?: GameVersion;
|
|
||||||
} = await invoke("fetch_game", {
|
|
||||||
gameId,
|
|
||||||
});
|
|
||||||
const meta = {
|
|
||||||
id: gameId,
|
|
||||||
version: data.version?.versionName,
|
|
||||||
downloadType: DownloadableType.Game
|
|
||||||
} satisfies DownloadableMetadata;
|
|
||||||
if (!gameRegistry[gameId]) {
|
if (!gameRegistry[gameId]) {
|
||||||
|
const data: {
|
||||||
gameRegistry[gameId] = { game: data.game, version: data.version };
|
game: Game;
|
||||||
}
|
status: SerializedGameStatus;
|
||||||
if (!downloadStatusRegistry.has(meta)) {
|
version?: GameVersion;
|
||||||
downloadStatusRegistry.set(meta, ref(parseStatus(data.status)));
|
} = await invoke("fetch_game", {
|
||||||
|
gameId,
|
||||||
listen(`update_game/${gameId}`, (event) => {
|
|
||||||
const payload: {
|
|
||||||
status: SerializedDownloadStatus;
|
|
||||||
version?: GameVersion;
|
|
||||||
} = event.payload as any;
|
|
||||||
|
|
||||||
downloadStatusRegistry.get(meta)!.value = parseStatus(payload.status);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* I am not super happy about this.
|
|
||||||
*
|
|
||||||
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
|
|
||||||
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
|
|
||||||
* on transient state updates.
|
|
||||||
*/
|
|
||||||
if (payload.version) {
|
|
||||||
gameRegistry[gameId].version = payload.version;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
gameRegistry[gameId] = { game: data.game, version: data.version };
|
||||||
|
if (!gameStatusRegistry[gameId]) {
|
||||||
|
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||||
|
|
||||||
|
listen(`update_game/${gameId}`, (event) => {
|
||||||
|
const payload: {
|
||||||
|
status: SerializedGameStatus;
|
||||||
|
version?: GameVersion;
|
||||||
|
} = event.payload as any;
|
||||||
|
console.log(payload.status);
|
||||||
|
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I am not super happy about this.
|
||||||
|
*
|
||||||
|
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
|
||||||
|
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
|
||||||
|
* on transient state updates.
|
||||||
|
*/
|
||||||
|
if (payload.version) {
|
||||||
|
gameRegistry[gameId].version = payload.version;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const game = gameRegistry[gameId];
|
const game = gameRegistry[gameId];
|
||||||
const status = downloadStatusRegistry.get(meta)!;
|
const status = gameStatusRegistry[gameId];
|
||||||
return { ...game, status };
|
return { ...game, status };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0-rc-2",
|
"version": "0.3.0-rc-3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
|
|||||||
@ -471,7 +471,6 @@ const router = useRouter();
|
|||||||
const id = route.params.id.toString();
|
const id = route.params.id.toString();
|
||||||
|
|
||||||
const { game: rawGame, status } = await useGame(id);
|
const { game: rawGame, status } = await useGame(id);
|
||||||
console.log("status: ", status);
|
|
||||||
const game = ref(rawGame);
|
const game = ref(rawGame);
|
||||||
|
|
||||||
const remoteUrl: string = await invoke("gen_drop_url", {
|
const remoteUrl: string = await invoke("gen_drop_url", {
|
||||||
|
|||||||
@ -1,32 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
|
||||||
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
|
<div
|
||||||
|
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
|
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
|
||||||
|
>
|
||||||
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||||
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
|
<span v-if="stats.time > 0" class="text-sm"
|
||||||
|
>{{ formatTime(stats.time) }} left</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||||
<div v-for="bar in speedHistory" :style="{ height: `${(bar / speedMax) * 100}%` }"
|
<div
|
||||||
class="w-[8px] bg-blue-600/40" />
|
v-for="bar in speedHistory"
|
||||||
|
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||||
|
class="w-[8px] bg-blue-600/40"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="queue.queue" @end="onEnd">
|
<draggable v-model="queue.queue" @end="onEnd">
|
||||||
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
|
||||||
<li v-if="downloads.has(element.meta)" :key="element.meta.id"
|
<li
|
||||||
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
|
v-if="games[element.meta.id]"
|
||||||
|
:key="element.meta.id"
|
||||||
|
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
|
||||||
|
>
|
||||||
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
<div class="w-full flex items-center max-w-md gap-x-4 relative">
|
||||||
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="downloads.get(element.meta)!.queueMeta.cover"
|
<img
|
||||||
alt="" />
|
class="size-24 flex-none bg-zinc-800 object-cover rounded"
|
||||||
|
:src="games[element.meta.id].cover"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<div class="min-w-0 flex-auto">
|
<div class="min-w-0 flex-auto">
|
||||||
<p class="text-xl font-semibold text-zinc-100">
|
<p class="text-xl font-semibold text-zinc-100">
|
||||||
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
<NuxtLink :href="`/library/${element.meta.id}`" class="">
|
||||||
<span class="absolute inset-x-0 -top-px bottom-0" />
|
<span class="absolute inset-x-0 -top-px bottom-0" />
|
||||||
{{ downloads.get(element.meta)!.queueMeta.mName }}
|
{{ games[element.meta.id].game.mName }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 flex text-xs/5 text-gray-500">
|
<p class="mt-1 flex text-xs/5 text-gray-500">
|
||||||
{{ downloads.get(element.meta)!.queueMeta.mShortDescription }}
|
{{ games[element.meta.id].game.mShortDescription }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -35,28 +49,40 @@
|
|||||||
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
<p class="text-md text-zinc-500 uppercase font-display font-bold">
|
||||||
{{ element.status }}
|
{{ element.status }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
|
<div
|
||||||
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
|
v-if="element.progress"
|
||||||
|
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-2 bg-blue-600"
|
||||||
|
:style="{ width: `${element.progress * 100}%` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"><span
|
<span
|
||||||
class="text-zinc-300">{{
|
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
||||||
formatKilobytes(element.current / 1000)
|
><span class="text-zinc-300">{{
|
||||||
}}</span>
|
formatKilobytes(element.current / 1000)
|
||||||
|
}}</span>
|
||||||
/
|
/
|
||||||
<span class="">{{ formatKilobytes(element.max / 1000) }}</span>
|
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
|
||||||
<ServerIcon class="size-5" />
|
><ServerIcon class="size-5"
|
||||||
</span>
|
/></span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="() => cancelGame(element.meta)" class="group">
|
<button @click="() => cancelGame(element.meta)" class="group">
|
||||||
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
<XMarkIcon
|
||||||
aria-hidden="true" />
|
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<p v-else>Loading...</p>
|
<p v-else>Loading...</p>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
|
<div
|
||||||
|
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
|
||||||
|
v-if="queue.queue.length == 0"
|
||||||
|
>
|
||||||
No items in the queue
|
No items in the queue
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,8 +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 { useStatus } from "~/composables/game";
|
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
|
||||||
import type { DownloadableMetadata, Game, GameStatus, QueueMetadata } from "~/types";
|
|
||||||
|
|
||||||
const windowWidth = ref(window.innerWidth);
|
const windowWidth = ref(window.innerWidth);
|
||||||
window.addEventListener("resize", (event) => {
|
window.addEventListener("resize", (event) => {
|
||||||
@ -82,7 +107,9 @@ const speedMax = computed(
|
|||||||
);
|
);
|
||||||
const previousGameId = ref<string | undefined>();
|
const previousGameId = ref<string | undefined>();
|
||||||
|
|
||||||
const downloads: Ref<Map<DownloadableMetadata, { queueMeta: QueueMetadata, status: Ref<GameStatus> }>> = ref(new Map());
|
const games: Ref<{
|
||||||
|
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||||
|
}> = ref({});
|
||||||
|
|
||||||
function resetHistoryGraph() {
|
function resetHistoryGraph() {
|
||||||
speedHistory.value = [];
|
speedHistory.value = [];
|
||||||
@ -126,14 +153,13 @@ watch(stats, (v) => {
|
|||||||
|
|
||||||
function loadGamesForQueue(v: typeof queue.value) {
|
function loadGamesForQueue(v: typeof queue.value) {
|
||||||
for (const {
|
for (const {
|
||||||
meta,
|
meta: { id },
|
||||||
} of v.queue) {
|
} of v.queue) {
|
||||||
if (downloads.value.get(meta)) return;
|
if (games.value[id]) return;
|
||||||
(async () => {
|
(async () => {
|
||||||
const queueMeta: QueueMetadata = await invoke("get_queue_metadata", {meta});
|
const gameData = await useGame(id);
|
||||||
const status = useStatus(meta)!;
|
const cover = await useObject(gameData.game.mCoverObjectId);
|
||||||
const cover = await useObject(queueMeta.cover);
|
games.value[id] = { ...gameData, cover };
|
||||||
downloads.value.set(meta, { queueMeta: { ...queueMeta, cover }, status });
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
Open Data Directory
|
Open Data Directory
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="() => queue_url_download()"
|
@click="() => openLogFile()"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
>
|
>
|
||||||
@ -115,15 +115,6 @@ dataDir.value = systemData.dataDir;
|
|||||||
const currentPlatform = await platform();
|
const currentPlatform = await platform();
|
||||||
platformInfo.value = currentPlatform;
|
platformInfo.value = currentPlatform;
|
||||||
|
|
||||||
async function queue_url_download() {
|
|
||||||
try {
|
|
||||||
await invoke("queue_url_download", { url: "https://codeload.github.com/Drop-OSS/drop-app/zip/refs/heads/develop"});
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDataDir() {
|
async function openDataDir() {
|
||||||
if (!dataDir.value) return;
|
if (!dataDir.value) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1247,7 +1247,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.0-rc-2"
|
version = "0.3.0-rc-3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-instant-full",
|
"atomic-instant-full",
|
||||||
"boxcar",
|
"boxcar",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.0-rc-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 = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use crate::AppState;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
client::url_downloader::URLDownloader, download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable}, error::download_manager_error::DownloadManagerError, AppState
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_state(
|
pub fn fetch_state(
|
||||||
@ -13,22 +9,3 @@ pub fn fetch_state(
|
|||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn queue_url_download(
|
|
||||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
|
||||||
url: String
|
|
||||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
|
||||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
|
||||||
let game_download_agent = Arc::new(Box::new(URLDownloader::new(
|
|
||||||
String::from("Test URL Download"),
|
|
||||||
"/home/quexeky/Downloads/test_url_download",
|
|
||||||
sender,
|
|
||||||
url,
|
|
||||||
)) as Box<dyn Downloadable + Send + Sync>);
|
|
||||||
Ok(state
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
|
||||||
.queue_download(game_download_agent)?)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
pub mod autostart;
|
pub mod autostart;
|
||||||
pub mod cleanup;
|
pub mod cleanup;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod url_downloader;
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
use std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{mpsc::Sender, Arc, Mutex},
|
|
||||||
usize,
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::{debug, error};
|
|
||||||
use reqwest::redirect::Policy;
|
|
||||||
use tauri::{AppHandle, Emitter};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::
|
|
||||||
models::data::{DownloadType, DownloadableMetadata}
|
|
||||||
,
|
|
||||||
download_manager::{
|
|
||||||
download_manager::{DownloadManagerSignal, DownloadStatus},
|
|
||||||
downloadable::Downloadable,
|
|
||||||
util::{
|
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
|
||||||
progress_object::{ProgressHandle, ProgressObject},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
games::downloads::download_logic::{DropDownloadPipeline, DropWriter},
|
|
||||||
DB,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct URLDownloader {
|
|
||||||
id: String,
|
|
||||||
version: String,
|
|
||||||
url: String,
|
|
||||||
control_flag: DownloadThreadControl,
|
|
||||||
progress: Arc<ProgressObject>,
|
|
||||||
target: PathBuf,
|
|
||||||
sender: Sender<DownloadManagerSignal>,
|
|
||||||
status: Mutex<DownloadStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct URLDownloaderManager {
|
|
||||||
current_offset: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl URLDownloader {
|
|
||||||
pub fn new<S: Into<String>, P: AsRef<Path>>(
|
|
||||||
id: String,
|
|
||||||
target: P,
|
|
||||||
sender: Sender<DownloadManagerSignal>,
|
|
||||||
url: S,
|
|
||||||
) -> Self {
|
|
||||||
// Don't run by default
|
|
||||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
version: String::new(),
|
|
||||||
control_flag,
|
|
||||||
target: target.as_ref().into(),
|
|
||||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
|
||||||
sender,
|
|
||||||
status: Mutex::new(DownloadStatus::Queued),
|
|
||||||
url: url.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn download(&self, _app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
|
||||||
// TODO: Fix these unwraps and implement From<io::Error> for ApplicationDownloadError
|
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.redirect(Policy::default())
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response = client.head(&self.url).send().unwrap();
|
|
||||||
let content_length = response
|
|
||||||
.headers()
|
|
||||||
.get(reqwest::header::CONTENT_LENGTH)
|
|
||||||
.map(|x| x.to_str().unwrap().parse().unwrap())
|
|
||||||
.unwrap_or(usize::MAX);
|
|
||||||
let response = client.get(&self.url).send().unwrap();
|
|
||||||
|
|
||||||
self.set_progress_object_params(content_length);
|
|
||||||
|
|
||||||
let progress = self.progress.get(0);
|
|
||||||
|
|
||||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
|
||||||
|
|
||||||
let mut pipeline = DropDownloadPipeline::new(
|
|
||||||
response,
|
|
||||||
DropWriter::new(&self.target),
|
|
||||||
&self.control_flag,
|
|
||||||
progress_handle,
|
|
||||||
content_length,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
let completed = pipeline
|
|
||||||
.copy()
|
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
|
||||||
if !completed {
|
|
||||||
return Ok(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
fn set_progress_object_params(&self, max: usize) {
|
|
||||||
// Avoid re-setting it
|
|
||||||
if self.progress.get_max() != 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.progress.set_max(max);
|
|
||||||
self.progress.set_size(1);
|
|
||||||
self.progress.set_time_now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Downloadable for URLDownloader {
|
|
||||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
|
||||||
self.download(app_handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn progress(&self) -> Arc<ProgressObject> {
|
|
||||||
self.progress.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn control_flag(&self) -> DownloadThreadControl {
|
|
||||||
self.control_flag.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(&self) -> DownloadableMetadata {
|
|
||||||
DownloadableMetadata {
|
|
||||||
id: self.id.clone(),
|
|
||||||
version: Some(self.version.clone()),
|
|
||||||
download_type: DownloadType::Tool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
|
||||||
app_handle
|
|
||||||
.emit("download_error", error.to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
error!("error while managing download: {}", error);
|
|
||||||
|
|
||||||
let mut handle = DB.borrow_data_mut().unwrap();
|
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.transient_statuses
|
|
||||||
.remove(&self.metadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_complete(&self, _app_handle: &tauri::AppHandle) {
|
|
||||||
debug!("Completed url download");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
|
||||||
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
|
|
||||||
debug!("Incomplete url download");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
|
||||||
debug!("Cancelled url download");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status(&self) -> DownloadStatus {
|
|
||||||
self.status.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,6 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||||
database::models::data::{DownloadType, DownloadableMetadata},
|
|
||||||
download_manager::download_manager::QueueMetadata,
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
@ -33,28 +29,3 @@ pub fn move_download_in_queue(
|
|||||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_queue_metadata(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
meta: DownloadableMetadata,
|
|
||||||
) -> Option<QueueMetadata> {
|
|
||||||
match meta.download_type {
|
|
||||||
DownloadType::Game => {
|
|
||||||
let state = state.lock().unwrap();
|
|
||||||
let game = state.games.get(&meta.id).unwrap();
|
|
||||||
Some(QueueMetadata {
|
|
||||||
cover: game.m_cover_object_id.clone(),
|
|
||||||
m_short_description: game.m_short_description.clone(),
|
|
||||||
m_name: game.m_name.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
DownloadType::Tool => Some(QueueMetadata {
|
|
||||||
cover: "IDK Man".to_string(),
|
|
||||||
m_short_description: "This is a tool".to_string(),
|
|
||||||
m_name: "Download".to_string(),
|
|
||||||
}),
|
|
||||||
DownloadType::DLC => unimplemented!(),
|
|
||||||
DownloadType::Mod => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -74,14 +74,6 @@ pub enum DownloadStatus {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct QueueMetadata {
|
|
||||||
pub cover: String,
|
|
||||||
pub m_short_description: String,
|
|
||||||
pub m_name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accessible front-end for the DownloadManager
|
/// Accessible front-end for the DownloadManager
|
||||||
///
|
///
|
||||||
/// The system works entirely through signals, both internally and externally,
|
/// The system works entirely through signals, both internally and externally,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use tauri::AppHandle;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
database::models::data::DownloadableMetadata,
|
||||||
error::{application_download_error::ApplicationDownloadError, remote_access_error::RemoteAccessError},
|
error::application_download_error::ApplicationDownloadError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -23,4 +23,3 @@ pub trait Downloadable: Send + Sync {
|
|||||||
fn on_incomplete(&self, app_handle: &AppHandle);
|
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,7 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj
|
|||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||||
use crate::games::library::{on_game_complete, push_game_update, Game, GameUpdateEvent};
|
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
|
||||||
use crate::remote::cache::get_cached_object;
|
|
||||||
use crate::remote::requests::make_request;
|
use crate::remote::requests::make_request;
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
|||||||
@ -11,7 +11,6 @@ use std::fs::{set_permissions, Permissions};
|
|||||||
use std::io::{ErrorKind, Read};
|
use std::io::{ErrorKind, Read};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::Path;
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||||
@ -23,14 +22,14 @@ pub struct DropWriter<W: Write> {
|
|||||||
destination: W,
|
destination: W,
|
||||||
}
|
}
|
||||||
impl DropWriter<File> {
|
impl DropWriter<File> {
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
fn new(path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
destination: OpenOptions::new().create(true).write(true).open(path).unwrap(),
|
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
||||||
hasher: Context::new(),
|
hasher: Context::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish(mut self) -> io::Result<Digest> {
|
fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush().unwrap();
|
self.flush().unwrap();
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.compute())
|
||||||
}
|
}
|
||||||
@ -67,7 +66,7 @@ pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
|||||||
pub size: usize,
|
pub size: usize,
|
||||||
}
|
}
|
||||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||||
pub fn new(
|
fn new(
|
||||||
source: Response,
|
source: Response,
|
||||||
destination: DropWriter<File>,
|
destination: DropWriter<File>,
|
||||||
control_flag: &'a DownloadThreadControl,
|
control_flag: &'a DownloadThreadControl,
|
||||||
@ -83,7 +82,7 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy(&mut self) -> Result<bool, io::Error> {
|
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||||
let copy_buf_size = 512;
|
let copy_buf_size = 512;
|
||||||
let mut copy_buf = vec![0; copy_buf_size];
|
let mut copy_buf = vec![0; copy_buf_size];
|
||||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||||
@ -103,10 +102,6 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
if current_size == self.size {
|
if current_size == self.size {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if bytes_read == 0 {
|
|
||||||
println!("Terminated stream");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
pub mod download_logic;
|
mod download_logic;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
mod stored_manifest;
|
mod stored_manifest;
|
||||||
|
|||||||
@ -31,14 +31,14 @@ pub struct FetchGameStruct {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
id: String,
|
id: String,
|
||||||
pub m_name: String,
|
m_name: String,
|
||||||
pub m_short_description: String,
|
m_short_description: String,
|
||||||
m_description: String,
|
m_description: String,
|
||||||
// mDevelopers
|
// mDevelopers
|
||||||
// mPublishers
|
// mPublishers
|
||||||
m_icon_object_id: String,
|
m_icon_object_id: String,
|
||||||
m_banner_object_id: String,
|
m_banner_object_id: String,
|
||||||
pub m_cover_object_id: String,
|
m_cover_object_id: String,
|
||||||
m_image_library_object_ids: Vec<String>,
|
m_image_library_object_ids: Vec<String>,
|
||||||
m_image_carousel_object_ids: Vec<String>,
|
m_image_carousel_object_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ mod error;
|
|||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
|
||||||
use crate::{client::commands::queue_url_download, database::db::DatabaseImpls, download_manager::commands::get_queue_metadata};
|
use crate::database::db::DatabaseImpls;
|
||||||
use client::{
|
use client::{
|
||||||
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||||
cleanup::{cleanup_and_exit, quit},
|
cleanup::{cleanup_and_exit, quit},
|
||||||
@ -264,8 +264,6 @@ pub fn run() {
|
|||||||
resume_downloads,
|
resume_downloads,
|
||||||
cancel_game,
|
cancel_game,
|
||||||
uninstall_game,
|
uninstall_game,
|
||||||
queue_url_download,
|
|
||||||
get_queue_metadata,
|
|
||||||
// Processes
|
// Processes
|
||||||
launch_game,
|
launch_game,
|
||||||
kill_game,
|
kill_game,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||||
"productName": "Drop Desktop Client",
|
"productName": "Drop Desktop Client",
|
||||||
"version": "0.3.0-rc-2",
|
"version": "0.3.0-rc-3",
|
||||||
"identifier": "dev.drop.app",
|
"identifier": "dev.drop.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn dev --port 1432",
|
"beforeDevCommand": "yarn dev --port 1432",
|
||||||
|
|||||||
10
types.ts
10
types.ts
@ -38,7 +38,6 @@ export type Game = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GameVersion = {
|
export type GameVersion = {
|
||||||
versionName: string;
|
|
||||||
launchCommandTemplate: string;
|
launchCommandTemplate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ export enum DownloadableType {
|
|||||||
|
|
||||||
export type DownloadableMetadata = {
|
export type DownloadableMetadata = {
|
||||||
id: string;
|
id: string;
|
||||||
version?: string;
|
version: string;
|
||||||
downloadType: DownloadableType;
|
downloadType: DownloadableType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,10 +84,3 @@ export type Settings = {
|
|||||||
maxDownloadThreads: number;
|
maxDownloadThreads: number;
|
||||||
forceOffline: boolean;
|
forceOffline: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueueMetadata = {
|
|
||||||
mName: string;
|
|
||||||
cover: string;
|
|
||||||
mShortDescription: string;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user