feat(download ui): debug queue interface

This commit is contained in:
DecDuck
2024-12-09 17:03:48 +11:00
parent d5ac1b0a0e
commit 671d45fbe4
16 changed files with 148 additions and 257 deletions

View File

@ -29,7 +29,6 @@
<div class="inline-flex items-center">
<ol class="inline-flex gap-3">
<HeaderQueueWidget
v-if="currentQueueObject"
:object="currentQueueObject"
/>
<li v-for="(item, itemIdx) in quickActions">

View File

@ -1,17 +1,26 @@
<script setup lang="ts">
import { ArrowDownTrayIcon } from "@heroicons/vue/20/solid";
const props = defineProps<{ object: QueueState["queue"][0] }>();
const props = defineProps<{ object?: QueueState["queue"][0] }>();
</script>
<template>
<div
class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 hover:bg-zinc-800 hover:text-zinc-300 relative"
<NuxtLink
to="/queue"
class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 hover:bg-zinc-800 relative"
>
<ArrowDownTrayIcon class="h-5 z-50 text-zinc-100 mix-blend-difference" />
<div
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
:style="{ width: `${props.object.progress * 100}%` }"
<ArrowDownTrayIcon
:class="[
'h-5 z-50',
props.object
? 'text-white hover:text-zinc-300'
: 'text-zinc-600 hover:text-zinc-300',
]"
/>
</div>
<div
v-if="props.object?.progress"
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
:style="{ width: `${props.object.progress * 99 + 1}%` }"
/>
</NuxtLink>
</template>

31
composables/game.ts Normal file
View File

@ -0,0 +1,31 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Game, GameStatus } from "~/types";
const gameRegistry: { [key: string]: Game } = {};
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
export const useGame = async (id: string) => {
if (!gameRegistry[id]) {
const data: { game: Game; status: GameStatus } = await invoke(
"fetch_game",
{
id,
}
);
gameRegistry[id] = data.game;
if (!gameStatusRegistry[id]) {
gameStatusRegistry[id] = ref(data.status);
listen(`update_game/${id}`, (event) => {
const payload: { status: GameStatus } = event.payload as any;
gameStatusRegistry[id].value = payload.status;
});
}
}
const game = gameRegistry[id];
const status = gameStatusRegistry[id];
return { game, status };
};

View File

@ -1,7 +1,7 @@
import { listen } from "@tauri-apps/api/event";
export type QueueState = {
queue: Array<{ id: string; status: string, progress: number }>;
queue: Array<{ id: string; status: string, progress: number | null }>;
};
export const useQueueState = () =>

View File

@ -14,7 +14,6 @@
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@prisma/client": "5.20.0",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
@ -22,14 +21,14 @@
"nuxt": "^3.13.0",
"scss": "^0.2.4",
"vue": "latest",
"vue-router": "latest"
"vue-router": "latest",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tauri-apps/cli": ">=2.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"prisma": "^5.20.0",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13"
},

View File

@ -40,12 +40,10 @@
</template>
<script setup lang="ts">
import type { Game } from "@prisma/client";
import { invoke } from "@tauri-apps/api/core";
import type { NavigationItem } from "~/types";
import type { Game, NavigationItem } from "~/types";
const rawGames = await invoke<string>("fetch_library");
const games: Array<Game> = JSON.parse(rawGames);
const games: Array<Game> = await invoke("fetch_library");
const icons = await Promise.all(games.map((e) => useObject(e.mIconId)));
const navigation = games.map((e) => {

View File

@ -318,25 +318,15 @@ import {
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { XCircleIcon } from "@heroicons/vue/24/solid";
import type { Game } from "@prisma/client";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { GameStatus } from "~/types";
import type { Game, GameStatus } from "~/types";
const route = useRoute();
const id = route.params.id;
const id = route.params.id.toString();
const raw: { game: Game; status: GameStatus } = JSON.parse(
await invoke<string>("fetch_game", { id: id })
);
const game = ref(raw.game);
const status = ref(raw.status);
listen(`update_game/${game.value.id}`, (event) => {
const payload: { status: GameStatus } = event.payload as any;
status.value = payload.status;
});
const { game: rawGame, status } = await useGame(id);
const game = ref(rawGame);
const bannerUrl = await useObject(game.value.mBannerId);

24
pages/queue.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<div class="text-white">
{{ element.id }}
</div>
</template>
</draggable>
{{ current }}
{{ rest }}
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const queue = useQueueState();
const current = computed(() => queue.value.queue.at(0));
const rest = computed(() => queue.value.queue.slice(1));
async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_game_in_queue", { oldIndex: event.oldIndex, newIndex: event.newIndex });
}
</script>

5
plugins/vuedraggable.ts Normal file
View File

@ -0,0 +1,5 @@
import draggable from "vuedraggable";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component("draggable", draggable);
});

View File

@ -1,150 +0,0 @@
// This should be copied from the main Drop repo
// TODO: do this automatically
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
username String @unique
admin Boolean @default(false)
email String
displayName String
profilePicture String // Object
authMecs LinkedAuthMec[]
clients Client[]
}
enum AuthMec {
Simple
}
model LinkedAuthMec {
userId String
mec AuthMec
credentials Json
user User @relation(fields: [userId], references: [id])
@@id([userId, mec])
}
enum ClientCapabilities {
DownloadAggregation
}
enum Platform {
Windows @map("windows")
Linux @map("linux")
}
// References a device
model Client {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
endpoint String
capabilities ClientCapabilities[]
name String
platform Platform
lastConnected DateTime
}
enum MetadataSource {
Custom
GiantBomb
}
model Game {
id String @id @default(uuid())
metadataSource MetadataSource
metadataId String
// Any field prefixed with m is filled in from metadata
// Acts as a cache so we can search and filter it
mName String // Name of game
mShortDescription String // Short description
mDescription String // Supports markdown
mDevelopers Developer[]
mPublishers Publisher[]
mReviewCount Int
mReviewRating Float
mIconId String // linked to objects in s3
mBannerId String // linked to objects in s3
mCoverId String
mImageLibrary String[] // linked to objects in s3
versions GameVersion[]
libraryBasePath String @unique // Base dir for all the game versions
@@unique([metadataSource, metadataId], name: "metadataKey")
}
// A particular set of files that relate to the version
model GameVersion {
gameId String
game Game @relation(fields: [gameId], references: [id])
versionName String // Sub directory for the game files
platform Platform
launchCommand String // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
setupCommand String // Command to setup game (dependencies and such)
dropletManifest Json // Results from droplet
versionIndex Int
delta Boolean @default(false)
@@id([gameId, versionName])
}
model Developer {
id String @id @default(uuid())
metadataSource MetadataSource
metadataId String
metadataOriginalQuery String
mName String
mShortDescription String
mDescription String
mLogo String
mBanner String
mWebsite String
games Game[]
@@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey")
}
model Publisher {
id String @id @default(uuid())
metadataSource MetadataSource
metadataId String
metadataOriginalQuery String
mName String
mShortDescription String
mDescription String
mLogo String
mBanner String
mWebsite String
games Game[]
@@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey")
}

View File

@ -19,21 +19,6 @@ pub fn download_game(
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
}
#[tauri::command]
pub fn get_current_game_download_progress(
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<f64, String> {
match state
.lock()
.unwrap()
.download_manager
.get_current_game_download_progress()
{
Some(progress) => Ok(progress),
None => Err("Game does not exist".to_string()),
}
}
#[tauri::command]
pub fn cancel_game_download(state: tauri::State<'_, Mutex<AppState>>, game_id: String) {
info!("Cancelling game download {}", game_id);
@ -54,6 +39,19 @@ pub fn resume_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
}
#[tauri::command]
pub fn move_game_in_queue(
state: tauri::State<'_, Mutex<AppState>>,
old_index: usize,
new_index: usize,
) {
state
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
}
#[tauri::command]
pub fn get_current_write_speed(state: tauri::State<'_, Mutex<AppState>>) {}

View File

@ -132,19 +132,23 @@ impl DownloadManager {
let current_index = get_index_from_id(&mut queue, id).unwrap();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
self.command_sender.send(DownloadManagerSignal::Update);
}
pub fn rearrange(&self, current_index: usize, new_index: usize) {
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
self.command_sender.send(DownloadManagerSignal::Update);
}
pub fn remove_from_queue(&self, index: usize) {
self.edit().remove(index);
self.command_sender.send(DownloadManagerSignal::Update);
}
pub fn remove_from_queue_string(&self, id: String) {
let mut queue = self.edit();
let current_index = get_index_from_id(&mut queue, id).unwrap();
queue.remove(current_index);
self.command_sender.send(DownloadManagerSignal::Update);
}
pub fn pause_downloads(&self) {
self.command_sender

View File

@ -134,7 +134,7 @@ pub fn run() {
fetch_game_verion_options,
// Downloads
download_game,
get_current_game_download_progress,
move_game_in_queue,
cancel_game_download,
pause_game_downloads,
resume_game_downloads,

View File

@ -17,7 +17,7 @@ use crate::remote::RemoteAccessError;
use crate::{auth::generate_authorization_header, AppState, DB};
#[derive(serde::Serialize)]
struct FetchGameStruct {
pub struct FetchGameStruct {
game: Game,
status: DatabaseGameStatus,
}
@ -67,7 +67,7 @@ pub struct GameVersionOption {
// total_size: usize,
}
fn fetch_library_logic(app: AppHandle) -> Result<String, RemoteAccessError> {
fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let library_url = base_url.join("/api/v1/client/user/library")?;
@ -102,15 +102,18 @@ fn fetch_library_logic(app: AppHandle) -> Result<String, RemoteAccessError> {
drop(handle);
Ok(json!(games.clone()).to_string())
Ok(games)
}
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<String, String> {
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, String> {
fetch_library_logic(app).map_err(|e| e.to_string())
}
fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result<String, RemoteAccessError> {
fn fetch_game_logic(
id: String,
app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> {
let state = app.state::<Mutex<AppState>>();
let mut state_handle = state.lock().unwrap();
@ -128,7 +131,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result<String, RemoteA
.clone(),
};
return Ok(json!(data).to_string());
return Ok(data);
}
let base_url = DB.fetch_base_url();
@ -173,11 +176,11 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result<String, RemoteA
.clone(),
};
return Ok(json!(data).to_string());
return Ok(data);
}
#[tauri::command]
pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result<String, String> {
pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
let result = fetch_game_logic(id, app);
if result.is_err() {

View File

@ -1,4 +1,3 @@
import type { User } from "@prisma/client";
import type { Component } from "vue";
export type NavigationItem = {
@ -12,11 +11,31 @@ export type QuickActionNav = {
notifications?: number;
action: () => Promise<void>;
};
export type User = {
id: string;
username: string;
admin: boolean;
displayName: string;
profilePicture: string;
};
export type AppState = {
status: AppStatus;
user?: User;
};
export type Game = {
id: string;
mName: string;
mShortDescription: string;
mDescription: string;
mIconId: string;
mBannerId: string;
mCoverId: string;
mImageLibrary: string[];
};
export enum AppStatus {
NotConfigured = "NotConfigured",
SignedOut = "SignedOut",

View File

@ -1075,47 +1075,6 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==
"@prisma/client@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.20.0.tgz#4fc9f2b2341c9c997c139df4445688dd6b39663b"
integrity sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==
"@prisma/debug@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.20.0.tgz#c6d1cf6e3c6e9dba150347f13ca200b1d66cc9fc"
integrity sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==
"@prisma/engines-version@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284":
version "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz#9a53b13cdcfd706ae54198111000f33c63655c39"
integrity sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==
"@prisma/engines@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.20.0.tgz#86fe407e55219d33d03ebc26dc829a422faed545"
integrity sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==
dependencies:
"@prisma/debug" "5.20.0"
"@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284"
"@prisma/fetch-engine" "5.20.0"
"@prisma/get-platform" "5.20.0"
"@prisma/fetch-engine@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz#b917880fb08f654981f14ca49923031b39683586"
integrity sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==
dependencies:
"@prisma/debug" "5.20.0"
"@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284"
"@prisma/get-platform" "5.20.0"
"@prisma/get-platform@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.20.0.tgz#c1a53a8d8af67f2b4a6b97dd4d25b1c603236804"
integrity sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==
dependencies:
"@prisma/debug" "5.20.0"
"@rollup/plugin-alias@^5.1.0":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz#53601d88cda8b1577aa130b4a6e452283605bf26"
@ -2816,7 +2775,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@2.3.3, fsevents@~2.3.2, fsevents@~2.3.3:
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
@ -4345,15 +4304,6 @@ pretty-bytes@^6.1.1:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b"
integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==
prisma@^5.20.0:
version "5.20.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.20.0.tgz#f2ab266a0d59383506886e7acbff0dbf322f4c7e"
integrity sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==
dependencies:
"@prisma/engines" "5.20.0"
optionalDependencies:
fsevents "2.3.3"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -4847,6 +4797,11 @@ smob@^1.0.0:
resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab"
integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==
sortablejs@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@ -5553,6 +5508,13 @@ vue@^3.5.5, vue@latest:
"@vue/server-renderer" "3.5.11"
"@vue/shared" "3.5.11"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"