From 2e5997525d0786eeb5b382aba6a75a881df1d8a2 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 7 Dec 2024 11:00:35 +1100 Subject: [PATCH] feat(download & db): combined db and download interface improvements --- desktop/components/GameStatusButton.vue | 72 +++++++++-------- desktop/pages/library/[id]/index.vue | 6 +- desktop/src-tauri/Cargo.lock | 17 ---- desktop/src-tauri/Cargo.toml | 12 +-- desktop/src-tauri/src/db.rs | 50 +++++++++--- .../src-tauri/src/downloads/download_agent.rs | 1 - .../src/downloads/download_manager_builder.rs | 19 +++-- desktop/src-tauri/src/lib.rs | 2 + desktop/src-tauri/src/library.rs | 79 +++++++++++++++++-- desktop/types.ts | 8 +- 10 files changed, 186 insertions(+), 80 deletions(-) diff --git a/desktop/components/GameStatusButton.vue b/desktop/components/GameStatusButton.vue index a7214425..972de001 100644 --- a/desktop/components/GameStatusButton.vue +++ b/desktop/components/GameStatusButton.vue @@ -1,18 +1,18 @@ @@ -22,9 +22,10 @@ import { PlayIcon, QueueListIcon, TrashIcon, + WrenchIcon, } from "@heroicons/vue/20/solid"; import type { Component } from "vue"; -import { GameStatus } from "~/types.js"; +import { GameStatusEnum, type GameStatus } from "~/types.js"; const props = defineProps<{ status: GameStatus }>(); const emit = defineEmits<{ @@ -33,43 +34,48 @@ const emit = defineEmits<{ (e: "play"): void; }>(); -const styles: { [key in GameStatus]: string } = { - [GameStatus.Remote]: +const styles: { [key in GameStatusEnum]: string } = { + [GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600", - [GameStatus.Queued]: + [GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700", - [GameStatus.Downloading]: + [GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700", - [GameStatus.Installed]: + [GameStatusEnum.SetupRequired]: + "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600", + [GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600", - [GameStatus.Updating]: "", - [GameStatus.Uninstalling]: "", + [GameStatusEnum.Updating]: "", + [GameStatusEnum.Uninstalling]: "", }; -const buttonNames: { [key in GameStatus]: string } = { - [GameStatus.Remote]: "Install", - [GameStatus.Queued]: "Queued", - [GameStatus.Downloading]: "Downloading", - [GameStatus.Installed]: "Play", - [GameStatus.Updating]: "Updating", - [GameStatus.Uninstalling]: "Uninstalling", +const buttonNames: { [key in GameStatusEnum]: string } = { + [GameStatusEnum.Remote]: "Install", + [GameStatusEnum.Queued]: "Queued", + [GameStatusEnum.Downloading]: "Downloading", + [GameStatusEnum.SetupRequired]: "Setup", + [GameStatusEnum.Installed]: "Play", + [GameStatusEnum.Updating]: "Updating", + [GameStatusEnum.Uninstalling]: "Uninstalling", }; -const buttonIcons: { [key in GameStatus]: Component } = { - [GameStatus.Remote]: ArrowDownTrayIcon, - [GameStatus.Queued]: QueueListIcon, - [GameStatus.Downloading]: ArrowDownTrayIcon, - [GameStatus.Installed]: PlayIcon, - [GameStatus.Updating]: ArrowDownTrayIcon, - [GameStatus.Uninstalling]: TrashIcon, +const buttonIcons: { [key in GameStatusEnum]: Component } = { + [GameStatusEnum.Remote]: ArrowDownTrayIcon, + [GameStatusEnum.Queued]: QueueListIcon, + [GameStatusEnum.Downloading]: ArrowDownTrayIcon, + [GameStatusEnum.SetupRequired]: WrenchIcon, + [GameStatusEnum.Installed]: PlayIcon, + [GameStatusEnum.Updating]: ArrowDownTrayIcon, + [GameStatusEnum.Uninstalling]: TrashIcon, }; -const buttonActions: { [key in GameStatus]: () => void } = { - [GameStatus.Remote]: () => emit("install"), - [GameStatus.Queued]: () => emit("cancel"), - [GameStatus.Downloading]: () => emit("cancel"), - [GameStatus.Installed]: () => emit("play"), - [GameStatus.Updating]: () => emit("cancel"), - [GameStatus.Uninstalling]: () => {}, +const buttonActions: { [key in GameStatusEnum]: () => void } = { + [GameStatusEnum.Remote]: () => emit("install"), + [GameStatusEnum.Queued]: () => emit("cancel"), + [GameStatusEnum.Downloading]: () => emit("cancel"), + [GameStatusEnum.SetupRequired]: () => {}, + [GameStatusEnum.Installed]: () => emit("play"), + [GameStatusEnum.Updating]: () => emit("cancel"), + [GameStatusEnum.Uninstalling]: () => {}, }; diff --git a/desktop/pages/library/[id]/index.vue b/desktop/pages/library/[id]/index.vue index b930d761..9d914ab2 100644 --- a/desktop/pages/library/[id]/index.vue +++ b/desktop/pages/library/[id]/index.vue @@ -275,7 +275,11 @@ > , // Guaranteed to exist if the game also exists in the app state map pub games_statuses: HashMap, + pub game_versions: HashMap>, } #[derive(Serialize, Clone, Deserialize)] @@ -50,8 +65,22 @@ pub struct Database { pub static DATA_ROOT_DIR: LazyLock> = LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); +// Custom JSON serializer to support everything we need +#[derive(Debug, Default, Clone)] +pub struct DropDatabaseSerializer; + +impl DeSerializer for DropDatabaseSerializer { + fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> { + serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string())) + } + + fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult { + serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string())) + } +} + pub type DatabaseInterface = - rustbreak::Database; + rustbreak::Database; pub trait DatabaseImpls { fn set_up_database() -> DatabaseInterface; @@ -80,6 +109,7 @@ impl DatabaseImpls for DatabaseInterface { games: DatabaseGames { install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], games_statuses: HashMap::new(), + game_versions: HashMap::new(), }, }; debug!("Creating database at path {}", db_path.as_str().unwrap()); diff --git a/desktop/src-tauri/src/downloads/download_agent.rs b/desktop/src-tauri/src/downloads/download_agent.rs index e6c630b9..2b3c26ac 100644 --- a/desktop/src-tauri/src/downloads/download_agent.rs +++ b/desktop/src-tauri/src/downloads/download_agent.rs @@ -187,7 +187,6 @@ impl GameDownloadAgent { drop(db_lock); let manifest = self.manifest.lock().unwrap().clone().unwrap(); - let version = self.version.clone(); let game_id = self.id.clone(); let data_base_dir_path = Path::new(&data_base_dir); diff --git a/desktop/src-tauri/src/downloads/download_manager_builder.rs b/desktop/src-tauri/src/downloads/download_manager_builder.rs index e9990dbe..a1578a50 100644 --- a/desktop/src-tauri/src/downloads/download_manager_builder.rs +++ b/desktop/src-tauri/src/downloads/download_manager_builder.rs @@ -11,7 +11,11 @@ use log::{error, info, warn}; use rustbreak::Database; use tauri::{AppHandle, Emitter}; -use crate::{db::DatabaseGameStatus, library::GameUpdateEvent, DB}; +use crate::{ + db::DatabaseGameStatus, + library::{on_game_complete, GameUpdateEvent}, + DB, +}; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, @@ -167,11 +171,11 @@ impl DownloadManagerBuilder { if interface.id == game_id { info!("Popping consumed data"); self.download_queue.pop_front(); - self.download_agent_registry.remove(&game_id); + let download_agent = self.download_agent_registry.remove(&game_id).unwrap(); self.active_control_flag = None; *self.progress.lock().unwrap() = None; - self.set_game_status(game_id, DatabaseGameStatus::Installed); + on_game_complete(game_id, download_agent.version.clone(), &self.app_handle); } } self.sender.send(DownloadManagerSignal::Go).unwrap(); @@ -190,11 +194,12 @@ impl DownloadManagerBuilder { id: id.clone(), status: Mutex::new(agent_status), }; + let version_name = download_agent.version.clone(); self.download_agent_registry .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); - self.set_game_status(id, DatabaseGameStatus::Queued); + self.set_game_status(id, DatabaseGameStatus::Queued { version_name }); } fn manage_go_signal(&mut self) { @@ -213,6 +218,8 @@ impl DownloadManagerBuilder { .clone(); self.current_game_interface = Some(agent_data); + let version_name = download_agent.version.clone(); + let progress_object = download_agent.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); @@ -240,7 +247,7 @@ impl DownloadManagerBuilder { self.set_status(DownloadManagerStatus::Downloading); self.set_game_status( self.current_game_interface.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Downloading, + DatabaseGameStatus::Downloading { version_name }, ); } fn manage_error_signal(&self, error: GameDownloadError) { @@ -251,7 +258,7 @@ impl DownloadManagerBuilder { self.set_game_status( self.current_game_interface.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Remote, + DatabaseGameStatus::Remote {}, ); } fn manage_cancel_signal(&mut self, game_id: String) { diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0d9a505c..ef307880 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(map_try_insert)] + mod auth; mod db; mod downloads; diff --git a/desktop/src-tauri/src/library.rs b/desktop/src-tauri/src/library.rs index 0afb0bc9..041cf2ef 100644 --- a/desktop/src-tauri/src/library.rs +++ b/desktop/src-tauri/src/library.rs @@ -1,14 +1,17 @@ +use std::collections::HashMap; use std::fmt::format; use std::sync::Mutex; use log::info; use serde::{Deserialize, Serialize}; use serde_json::json; +use tauri::Emitter; use tauri::{AppHandle, Manager}; +use urlencoding::encode; -use crate::db; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; +use crate::db::{self, GameVersion}; use crate::downloads::download_manager::GameDownloadStatus; use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; @@ -68,7 +71,7 @@ fn fetch_library_logic(app: AppHandle) -> Result { return Err(response.status().as_u16().into()); } - let games = response.json::>()?; + let games: Vec = response.json::>()?; let state = app.state::>(); let mut handle = state.lock().unwrap(); @@ -81,7 +84,7 @@ fn fetch_library_logic(app: AppHandle) -> Result { db_handle .games .games_statuses - .insert(game.id.clone(), DatabaseGameStatus::Remote); + .insert(game.id.clone(), DatabaseGameStatus::Remote {}); } } @@ -145,7 +148,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result Result { .games .games_statuses .get(&id) - .unwrap_or(&DatabaseGameStatus::Remote) + .unwrap_or(&DatabaseGameStatus::Remote {}) .clone(); drop(db_handle); return Ok(status); } -fn fetch_game_verion_options_logic(game_id: String) -> Result, RemoteAccessError> { +fn fetch_game_verion_options_logic( + game_id: String, +) -> Result, RemoteAccessError> { let base_url = DB.fetch_base_url(); let endpoint = @@ -214,3 +219,65 @@ fn fetch_game_verion_options_logic(game_id: String) -> Result Result, String> { fetch_game_verion_options_logic(game_id).map_err(|e| e.to_string()) } + +pub fn on_game_complete( + game_id: String, + version_name: String, + app_handle: &AppHandle, +) -> Result<(), RemoteAccessError> { + // Fetch game version information from remote + let base_url = DB.fetch_base_url(); + + let endpoint = base_url.join( + format!( + "/api/v1/client/metadata/version?id={}&version={}", + game_id, + encode(&version_name) + ) + .as_str(), + )?; + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; + + let data = response.json::()?; + + let mut handle = DB.borrow_data_mut().unwrap(); + handle + .games + .game_versions + .entry(game_id.clone()) + .or_insert(HashMap::new()) + .insert(version_name.clone(), data.clone()); + drop(handle); + DB.save().unwrap(); + + let status = if data.setup_command.is_empty() { + DatabaseGameStatus::Installed { version_name } + } else { + DatabaseGameStatus::SetupRequired { version_name } + }; + + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle + .games + .games_statuses + .insert(game_id.clone(), status.clone()); + drop(db_handle); + DB.save().unwrap(); + app_handle + .emit( + &format!("update_game/{}", game_id), + GameUpdateEvent { + game_id: game_id, + status: status, + }, + ) + .unwrap(); + + Ok(()) +} diff --git a/desktop/types.ts b/desktop/types.ts index 9b91f485..3a6e3f46 100644 --- a/desktop/types.ts +++ b/desktop/types.ts @@ -25,11 +25,17 @@ export enum AppStatus { ServerUnavailable = "ServerUnavailable", } -export enum GameStatus { +export enum GameStatusEnum { Remote = "Remote", Queued = "Queued", Downloading = "Downloading", Installed = "Installed", Updating = "Updating", Uninstalling = "Uninstalling", + SetupRequired = "SetupRequired", } + +export type GameStatus = { + type: GameStatusEnum; + version_name?: string; +};