refactor: Converting useGame to DownloadableMetadata

Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
quexeky
2025-06-05 12:59:45 +10:00
parent beea0505d1
commit 9b68ebc910
11 changed files with 144 additions and 114 deletions

View File

@ -1,19 +1,19 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
import { type Game, type GameStatus as DownloadStatus, type GameStatusEnum as DownloadStatusEnum, type GameVersion, type DownloadableMetadata, DownloadableType } from "~/types";
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
{};
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
const downloadStatusRegistry: Map<DownloadableMetadata, Ref<DownloadStatus>> = new Map();
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
export type SerializedGameStatus = [
{ type: GameStatusEnum },
OptionGameStatus | null
type OptionDownloadStatus = { [key in DownloadStatusEnum]: { version_name?: string } };
export type SerializedDownloadStatus = [
{ type: DownloadStatusEnum },
OptionDownloadStatus | null
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
export const parseStatus = (status: SerializedDownloadStatus): DownloadStatus => {
console.log(status);
if (status[0]) {
return {
@ -22,7 +22,7 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
} else if (status[1]) {
const [[gameStatus, options]] = Object.entries(status[1]);
return {
type: gameStatus as GameStatusEnum,
type: gameStatus as DownloadStatusEnum,
...options,
};
} else {
@ -30,43 +30,54 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
}
};
export const useGame = async (gameId: string) => {
if (!gameRegistry[gameId]) {
const data: {
game: Game;
status: SerializedGameStatus;
version?: GameVersion;
} = await invoke("fetch_game", {
gameId,
});
gameRegistry[gameId] = { game: data.game, version: data.version };
if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
export const useStatus = (meta: DownloadableMetadata) => {
return downloadStatusRegistry.get(meta)
}
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;
}
});
}
export const useGame = async (gameId: string) => {
const data: {
game: Game;
status: SerializedDownloadStatus;
version?: GameVersion;
} = await invoke("fetch_game", {
gameId,
});
const meta = {
id: gameId,
version: data.version?.versionName,
downloadType: DownloadableType.Game
} satisfies DownloadableMetadata;
if (!gameRegistry[gameId]) {
gameRegistry[gameId] = { game: data.game, version: data.version };
}
if (!downloadStatusRegistry.has(meta)) {
downloadStatusRegistry.set(meta, ref(parseStatus(data.status)));
listen(`update_game/${gameId}`, (event) => {
const payload: {
status: SerializedDownloadStatus;
version?: GameVersion;
} = event.payload as any;
downloadStatusRegistry.get(meta)!.value = parseStatus(payload.status);
/**
* I am not super happy about this.
*
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
* on transient state updates.
*/
if (payload.version) {
gameRegistry[gameId].version = payload.version;
}
});
}
const game = gameRegistry[gameId];
const status = gameStatusRegistry[gameId];
const status = downloadStatusRegistry.get(meta)!;
return { ...game, status };
};

View File

@ -471,6 +471,7 @@ const router = useRouter();
const id = route.params.id.toString();
const { game: rawGame, status } = await useGame(id);
console.log("status: ", status);
const game = ref(rawGame);
const remoteUrl: string = await invoke("gen_drop_url", {

View File

@ -1,46 +1,32 @@
<template>
<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
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 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 class="absolute inset-0 h-full flex flex-row items-end justify-end">
<div
v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40"
/>
<div v-for="bar in speedHistory" :style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40" />
</div>
</div>
<draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<li
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"
>
<li v-if="downloads.has(element.meta)" :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">
<img
class="size-24 flex-none bg-zinc-800 object-cover rounded"
:src="games[element.meta.id].cover"
alt=""
/>
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="downloads.get(element.meta)!.queueMeta.cover"
alt="" />
<div class="min-w-0 flex-auto">
<p class="text-xl font-semibold text-zinc-100">
<NuxtLink :href="`/library/${element.meta.id}`" class="">
<span class="absolute inset-x-0 -top-px bottom-0" />
{{ games[element.meta.id].game.mName }}
{{ downloads.get(element.meta)!.queueMeta.mName }}
</NuxtLink>
</p>
<p class="mt-1 flex text-xs/5 text-gray-500">
{{ games[element.meta.id].game.mShortDescription }}
{{ downloads.get(element.meta)!.queueMeta.mShortDescription }}
</p>
</div>
</div>
@ -49,40 +35,28 @@
<p class="text-md text-zinc-500 uppercase font-display font-bold">
{{ element.status }}
</p>
<div
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 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>
<span
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}</span>
<span class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"><span
class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}</span>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
><ServerIcon class="size-5"
/></span>
<span class="">{{ formatKilobytes(element.max / 1000) }}</span>
<ServerIcon class="size-5" />
</span>
</div>
<button @click="() => cancelGame(element.meta)" class="group">
<XMarkIcon
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true"
/>
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true" />
</button>
</div>
</li>
<p v-else>Loading...</p>
</template>
</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
</div>
</div>
@ -91,7 +65,8 @@
<script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
import { useStatus } from "~/composables/game";
import type { DownloadableMetadata, Game, GameStatus, QueueMetadata } from "~/types";
const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => {
@ -107,9 +82,7 @@ const speedMax = computed(
);
const previousGameId = ref<string | undefined>();
const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
}> = ref({});
const downloads: Ref<Map<DownloadableMetadata, { queueMeta: QueueMetadata, status: Ref<GameStatus> }>> = ref(new Map());
function resetHistoryGraph() {
speedHistory.value = [];
@ -153,13 +126,14 @@ watch(stats, (v) => {
function loadGamesForQueue(v: typeof queue.value) {
for (const {
meta: { id },
meta,
} of v.queue) {
if (games.value[id]) return;
if (downloads.value.get(meta)) return;
(async () => {
const gameData = await useGame(id);
const cover = await useObject(gameData.game.mCoverObjectId);
games.value[id] = { ...gameData, cover };
const queueMeta: QueueMetadata = await invoke("get_queue_metadata", {meta});
const status = useStatus(meta)!;
const cover = await useObject(queueMeta.cover);
downloads.value.set(meta, { queueMeta: { ...queueMeta, cover }, status });
})();
}
}

View File

@ -1,9 +1,7 @@
use std::{
fs::{self, create_dir_all},
io::{self, copy},
path::{Path, PathBuf},
sync::{mpsc::Sender, Arc, Mutex},
u64, usize,
usize,
};
use log::{debug, error, warn};
@ -65,7 +63,7 @@ impl URLDownloader {
}
}
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
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())
@ -80,8 +78,6 @@ impl URLDownloader {
.unwrap_or(usize::MAX);
let response = client.get(&self.url).send().unwrap();
println!("{:?}, content length: {}", response, content_length);
self.set_progress_object_params(content_length);
let progress = self.progress.get(0);
@ -159,7 +155,7 @@ impl Downloadable for URLDownloader {
.remove(&self.metadata());
}
fn on_complete(&self, app_handle: &tauri::AppHandle) {
fn on_complete(&self, _app_handle: &tauri::AppHandle) {
println!("Completed url download");
}

View File

@ -1,6 +1,10 @@
use std::sync::Mutex;
use crate::{database::models::data::DownloadableMetadata, AppState};
use crate::{
database::models::data::{DownloadType, DownloadableMetadata},
download_manager::download_manager::QueueMetadata,
AppState,
};
#[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
@ -29,3 +33,28 @@ pub fn move_download_in_queue(
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
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(),
name: game.m_name.clone(),
})
}
DownloadType::Tool => Some(QueueMetadata {
cover: "IDK Man".to_string(),
m_short_description: "This is a tool".to_string(),
name: "Download".to_string(),
}),
DownloadType::DLC => unimplemented!(),
DownloadType::Mod => unimplemented!(),
}
}

View File

@ -74,6 +74,14 @@ pub enum DownloadStatus {
Error,
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueueMetadata {
pub cover: String,
pub m_short_description: String,
pub name: String
}
/// Accessible front-end for the DownloadManager
///
/// The system works entirely through signals, both internally and externally,

View File

@ -4,7 +4,7 @@ use tauri::AppHandle;
use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
error::{application_download_error::ApplicationDownloadError, remote_access_error::RemoteAccessError},
};
use super::{
@ -23,3 +23,4 @@ pub trait Downloadable: Send + Sync {
fn on_incomplete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle);
}

View File

@ -10,7 +10,8 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
use crate::games::library::{on_game_complete, push_game_update, Game, GameUpdateEvent};
use crate::remote::cache::get_cached_object;
use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info};

View File

@ -31,14 +31,14 @@ pub struct FetchGameStruct {
#[serde(rename_all = "camelCase")]
pub struct Game {
id: String,
m_name: String,
m_short_description: String,
pub m_name: String,
pub m_short_description: String,
m_description: String,
// mDevelopers
// mPublishers
m_icon_object_id: String,
m_banner_object_id: String,
m_cover_object_id: String,
pub m_cover_object_id: String,
m_image_library_object_ids: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
}

View File

@ -7,7 +7,7 @@ mod error;
mod process;
mod remote;
use crate::{client::commands::queue_url_download, database::db::DatabaseImpls};
use crate::{client::commands::queue_url_download, database::db::DatabaseImpls, download_manager::commands::get_queue_metadata};
use client::{
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
cleanup::{cleanup_and_exit, quit},
@ -265,6 +265,7 @@ pub fn run() {
cancel_game,
uninstall_game,
queue_url_download,
get_queue_metadata,
// Processes
launch_game,
kill_game,

View File

@ -38,6 +38,7 @@ export type Game = {
};
export type GameVersion = {
versionName: string;
launchCommandTemplate: string;
};
@ -75,7 +76,7 @@ export enum DownloadableType {
export type DownloadableMetadata = {
id: string;
version: string;
version?: string;
downloadType: DownloadableType;
};
@ -84,3 +85,10 @@ export type Settings = {
maxDownloadThreads: number;
forceOffline: boolean;
};
export type QueueMetadata = {
mName: string;
cover: string;
mShortDescription: string;
}