From 42c0198f1d95a2e95ec53489f572c3e038c33393 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 23 Dec 2024 20:44:02 +1100 Subject: [PATCH] refactor(game status): transient vs synced state now defined --- components/GameStatusButton.vue | 8 +- composables/game.ts | 34 ++++++-- composables/state-navigation.ts | 4 + pages/library/[id]/index.vue | 80 +++++-------------- pages/queue.vue | 21 +++-- src-tauri/src/db.rs | 27 ++++--- src-tauri/src/downloads/download_commands.rs | 5 ++ src-tauri/src/downloads/download_manager.rs | 10 ++- .../src/downloads/download_manager_builder.rs | 65 ++++++++++++--- src-tauri/src/lib.rs | 3 +- src-tauri/src/library.rs | 68 +++++++--------- src-tauri/src/process/process_manager.rs | 8 +- src-tauri/src/settings.rs | 1 - src-tauri/src/state.rs | 31 +++++++ 14 files changed, 220 insertions(+), 145 deletions(-) delete mode 100644 src-tauri/src/settings.rs create mode 100644 src-tauri/src/state.rs diff --git a/components/GameStatusButton.vue b/components/GameStatusButton.vue index 972de00..a71fca4 100644 --- a/components/GameStatusButton.vue +++ b/components/GameStatusButton.vue @@ -30,8 +30,8 @@ import { GameStatusEnum, type GameStatus } from "~/types.js"; const props = defineProps<{ status: GameStatus }>(); const emit = defineEmits<{ (e: "install"): void; - (e: "cancel"): void; (e: "play"): void; + (e: "queue"): void; }>(); const styles: { [key in GameStatusEnum]: string } = { @@ -71,11 +71,11 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = { const buttonActions: { [key in GameStatusEnum]: () => void } = { [GameStatusEnum.Remote]: () => emit("install"), - [GameStatusEnum.Queued]: () => emit("cancel"), - [GameStatusEnum.Downloading]: () => emit("cancel"), + [GameStatusEnum.Queued]: () => emit("queue"), + [GameStatusEnum.Downloading]: () => emit("queue"), [GameStatusEnum.SetupRequired]: () => {}, [GameStatusEnum.Installed]: () => emit("play"), - [GameStatusEnum.Updating]: () => emit("cancel"), + [GameStatusEnum.Updating]: () => emit("queue"), [GameStatusEnum.Uninstalling]: () => {}, }; diff --git a/composables/game.ts b/composables/game.ts index 5eba1d9..7f28435 100644 --- a/composables/game.ts +++ b/composables/game.ts @@ -1,14 +1,36 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import type { Game, GameStatus } from "~/types"; +import type { Game, GameStatus, GameStatusEnum } from "~/types"; const gameRegistry: { [key: string]: Game } = {}; const gameStatusRegistry: { [key: string]: Ref } = {}; +type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } }; +export type SerializedGameStatus = [ + { type: GameStatusEnum }, + OptionGameStatus | null +]; + +const parseStatus = (status: SerializedGameStatus): GameStatus => { + if (status[0]) { + return { + type: status[0].type, + }; + } else if (status[1]) { + const [[gameStatus, options]] = Object.entries(status[1]); + return { + type: gameStatus as GameStatusEnum, + ...options, + }; + } else { + throw new Error("No game status"); + } +}; + export const useGame = async (id: string) => { if (!gameRegistry[id]) { - const data: { game: Game; status: GameStatus } = await invoke( + const data: { game: Game; status: SerializedGameStatus } = await invoke( "fetch_game", { id, @@ -16,11 +38,13 @@ export const useGame = async (id: string) => { ); gameRegistry[id] = data.game; if (!gameStatusRegistry[id]) { - gameStatusRegistry[id] = ref(data.status); + gameStatusRegistry[id] = ref(parseStatus(data.status)); listen(`update_game/${id}`, (event) => { - const payload: { status: GameStatus } = event.payload as any; - gameStatusRegistry[id].value = payload.status; + const payload: { + status: SerializedGameStatus; + } = event.payload as any; + gameStatusRegistry[id].value = parseStatus(payload.status); }); } } diff --git a/composables/state-navigation.ts b/composables/state-navigation.ts index 4658eb4..790d258 100644 --- a/composables/state-navigation.ts +++ b/composables/state-navigation.ts @@ -18,10 +18,14 @@ export function setupHooks() { router.push("/store"); }); + /* + document.addEventListener("contextmenu", (event) => { event.target?.dispatchEvent(new Event("contextmenu")); event.preventDefault(); }); + + */ } export function initialNavigation(state: Ref) { diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index c5678ec..4456b11 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -6,7 +6,7 @@

{{ game.mName }}

@@ -17,40 +17,23 @@
-
+
-
+ + @@ -349,45 +332,23 @@ import { ListboxOptions, } from "@headlessui/vue"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; +import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline"; import { XCircleIcon } from "@heroicons/vue/24/solid"; import { invoke } from "@tauri-apps/api/core"; -import MarkdownIt from "markdown-it"; -import moment from "moment"; const route = useRoute(); +const router = useRouter(); const id = route.params.id.toString(); const { game: rawGame, status } = await useGame(id); const game = ref(rawGame); +const remoteUrl: string = await invoke("gen_drop_url", { + path: `/store/${game.value.id}`, +}); + const bannerUrl = await useObject(game.value.mBannerId); -const md = MarkdownIt(); - -const showPreview = ref(true); -const gameDescriptionCharacters = game.value.mDescription.split(""); - -// First new line after x characters -const descriptionSplitIndex = gameDescriptionCharacters.findIndex( - (v, i, arr) => { - // If we're at the last element, we return true. - // So we don't have to handle a -1 from this findIndex - if (i + 1 == arr.length) return true; - if (i < 500) return false; - if (v != "\n") return false; - return true; - } -); - -const previewDescription = gameDescriptionCharacters - .slice(0, descriptionSplitIndex + 1) // Slice a character after - .join(""); -const previewHTML = md.render(previewDescription); - -const descriptionHTML = md.render(game.value.mDescription); - -const showReadMore = previewHTML != descriptionHTML; - const installFlowOpen = ref(false); const versionOptions = ref< undefined | Array<{ versionName: string; platform: string }> @@ -432,8 +393,11 @@ async function play() { try { await invoke("launch_game", { gameId: game.value.id }); } catch (e) { - game.value.mName = e as string; console.error(e); } } + +async function queue() { + router.push("/queue"); +} diff --git a/pages/queue.vue b/pages/queue.vue index d00466c..6572203 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -5,9 +5,9 @@
  • -
    +

    - + {{ games[element.id].game.mName }} @@ -40,10 +40,12 @@ />

    -
  • Loading...

    @@ -59,6 +61,7 @@ diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index b89d2ce..00e93ab 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -24,11 +24,8 @@ pub struct DatabaseAuth { // Strings are version names for a particular game #[derive(Serialize, Clone, Deserialize)] #[serde(tag = "type")] -pub enum DatabaseGameStatus { +pub enum GameStatus { Remote {}, - Downloading { - version_name: String, - }, SetupRequired { version_name: String, install_dir: String, @@ -37,10 +34,14 @@ pub enum DatabaseGameStatus { version_name: String, install_dir: String, }, - Updating { - version_name: String, - }, +} + +// Stuff that shouldn't be synced to disk +#[derive(Clone, Serialize)] +pub enum GameTransientStatus { + Downloading { version_name: String }, Uninstalling {}, + Updating { version_name: String }, } #[derive(Serialize, Deserialize, Clone)] @@ -58,8 +59,11 @@ pub struct GameVersion { pub struct DatabaseGames { pub install_dirs: Vec, // Guaranteed to exist if the game also exists in the app state map - pub games_statuses: HashMap, - pub game_versions: HashMap>, + pub statuses: HashMap, + pub versions: HashMap>, + + #[serde(skip)] + pub transient_statuses: HashMap, } #[derive(Serialize, Clone, Deserialize)] @@ -119,8 +123,9 @@ impl DatabaseImpls for DatabaseInterface { base_url: "".to_string(), games: DatabaseGames { install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], - games_statuses: HashMap::new(), - game_versions: HashMap::new(), + statuses: HashMap::new(), + transient_statuses: HashMap::new(), + versions: HashMap::new(), }, }; debug!( diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 23b8bab..d3ca2e7 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -40,6 +40,11 @@ pub fn move_game_in_queue( .rearrange(old_index, new_index) } +#[tauri::command] +pub fn cancel_game(state: tauri::State<'_, Mutex>, game_id: String) { + state.lock().unwrap().download_manager.cancel(game_id) +} + /* #[tauri::command] pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 36313a9..8676087 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -33,7 +33,10 @@ pub enum DownloadManagerSignal { /// download, sync everything to disk, and /// then exit Finish, + /// Stops (but doesn't remove) current download Cancel, + /// Removes a given game + Remove(String), /// Any error which occurs in the agent Error(GameDownloadError), /// Pushes UI update @@ -142,6 +145,11 @@ impl DownloadManager { .send(DownloadManagerSignal::Update) .unwrap(); } + pub fn cancel(&self, game_id: String) { + self.command_sender + .send(DownloadManagerSignal::Remove(game_id)) + .unwrap(); + } pub fn rearrange(&self, current_index: usize, new_index: usize) { if current_index == new_index { return; @@ -159,8 +167,8 @@ impl DownloadManager { let mut queue = self.edit(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); - info!("new queue: {:?}", queue); + drop(queue); if needs_pause { self.command_sender.send(DownloadManagerSignal::Go).unwrap(); diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 9e42598..c24bbe3 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, sync::{ mpsc::{channel, Receiver, Sender}, - Arc, Mutex, + Arc, Mutex, RwLockWriteGuard, }, thread::{spawn, JoinHandle}, }; @@ -11,8 +11,9 @@ use log::{error, info}; use tauri::{AppHandle, Emitter}; use crate::{ - db::DatabaseGameStatus, + db::{Database, GameStatus, GameTransientStatus}, library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData}, + state::GameStatusManager, DB, }; @@ -107,14 +108,18 @@ impl DownloadManagerBuilder { DownloadManager::new(terminator, queue, active_progress, command_sender) } - fn set_game_status(&self, id: String, status: DatabaseGameStatus) { + fn set_game_status, &String) -> ()>( + &self, + id: String, + setter: F, + ) { let mut db_handle = DB.borrow_data_mut().unwrap(); - db_handle - .games - .games_statuses - .insert(id.clone(), status.clone()); + setter(&mut db_handle, &id); drop(db_handle); DB.save().unwrap(); + + let status = GameStatusManager::fetch_state(&id); + self.app_handle .emit( &format!("update_game/{}", id), @@ -208,10 +213,35 @@ impl DownloadManagerBuilder { self.stop_and_wait_current_download(); return Ok(()); } + DownloadManagerSignal::Remove(game_id) => { + self.manage_remove_game(game_id); + } }; } } + fn manage_remove_game(&mut self, game_id: String) { + if let Some(current_download) = &self.current_download_agent { + if current_download.id == game_id { + self.manage_cancel_signal(); + } + } + + let index = self.download_queue.get_by_id(game_id.clone()).unwrap(); + let mut queue_handle = self.download_queue.edit(); + queue_handle.remove(index); + self.set_game_status(game_id, |db_handle, id| { + db_handle.games.transient_statuses.remove(id); + }); + drop(queue_handle); + + if self.current_download_agent.is_none() { + self.manage_go_signal(); + } + + self.push_manager_update(); + } + fn manage_stop_signal(&mut self) { info!("Got signal 'Stop'"); self.set_status(DownloadManagerStatus::Paused); @@ -273,7 +303,12 @@ impl DownloadManagerBuilder { .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); - self.set_game_status(id, DatabaseGameStatus::Downloading { version_name }); + self.set_game_status(id, |db, id| { + db.games.transient_statuses.insert( + id.to_string(), + GameTransientStatus::Downloading { version_name }, + ); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } @@ -344,10 +379,12 @@ impl DownloadManagerBuilder { // Set flags for download manager active_control_flag.set(DownloadThreadControlFlag::Go); self.set_status(DownloadManagerStatus::Downloading); - self.set_game_status( - self.current_download_agent.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Downloading { version_name }, - ); + self.set_game_status(agent_data.id.clone(), |db, id| { + db.games.transient_statuses.insert( + id.to_string(), + GameTransientStatus::Downloading { version_name }, + ); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } @@ -361,7 +398,9 @@ impl DownloadManagerBuilder { self.set_status(DownloadManagerStatus::Error(error)); let game_id = current_status.id.clone(); - self.set_game_status(game_id, DatabaseGameStatus::Remote {}); + self.set_game_status(game_id, |db_handle, id| { + db_handle.games.transient_statuses.remove(id); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6e25768..0d19ea6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ mod library; mod process; mod remote; -mod settings; +mod state; #[cfg(test)] mod tests; @@ -178,6 +178,7 @@ pub fn run() { move_game_in_queue, pause_game_downloads, resume_game_downloads, + cancel_game, // Processes launch_game, ]) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index d4d7f63..6e8536e 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -5,18 +5,19 @@ use tauri::Emitter; use tauri::{AppHandle, Manager}; use urlencoding::encode; -use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::db::GameVersion; +use crate::db::{GameStatus, GameTransientStatus}; use crate::downloads::download_manager::GameDownloadStatus; use crate::process::process_manager::Platform; use crate::remote::RemoteAccessError; +use crate::state::{GameStatusManager, GameStatusWithTransient}; use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] pub struct FetchGameStruct { game: Game, - status: DatabaseGameStatus, + status: GameStatusWithTransient, } #[derive(Serialize, Deserialize, Clone)] @@ -36,7 +37,7 @@ pub struct Game { #[derive(serde::Serialize, Clone)] pub struct GameUpdateEvent { pub game_id: String, - pub status: DatabaseGameStatus, + pub status: (Option, Option), } #[derive(Serialize, Clone)] @@ -61,6 +62,7 @@ pub struct GameVersionOption { setup_command: String, launch_command: String, delta: bool, + umu_id_override: Option, // total_size: usize, } @@ -89,11 +91,11 @@ fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { for game in games.iter() { handle.games.insert(game.id.clone(), game.clone()); - if !db_handle.games.games_statuses.contains_key(&game.id) { + if !db_handle.games.statuses.contains_key(&game.id) { db_handle .games - .games_statuses - .insert(game.id.clone(), DatabaseGameStatus::Remote {}); + .statuses + .insert(game.id.clone(), GameStatus::Remote {}); } } @@ -116,16 +118,11 @@ fn fetch_game_logic( let game = state_handle.games.get(&id); if let Some(game) = game { - let db_handle = DB.borrow_data().unwrap(); + let status = GameStatusManager::fetch_state(&id); let data = FetchGameStruct { game: game.clone(), - status: db_handle - .games - .games_statuses - .get(&game.id) - .unwrap() - .clone(), + status, }; return Ok(data); @@ -158,28 +155,23 @@ fn fetch_game_logic( db_handle .games - .games_statuses - .entry(id) - .or_insert(DatabaseGameStatus::Remote {}); + .statuses + .entry(id.clone()) + .or_insert(GameStatus::Remote {}); + drop(db_handle); + + let status = GameStatusManager::fetch_state(&id); let data = FetchGameStruct { game: game.clone(), - status: db_handle - .games - .games_statuses - .get(&game.id) - .unwrap() - .clone(), + status, }; Ok(data) } #[tauri::command] -pub fn fetch_game( - id: String, - app: tauri::AppHandle, -) -> Result { +pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { let result = fetch_game_logic(id, app); if result.is_err() { @@ -190,15 +182,8 @@ pub fn fetch_game( } #[tauri::command] -pub fn fetch_game_status(id: String) -> Result { - let db_handle = DB.borrow_data().unwrap(); - let status = db_handle - .games - .games_statuses - .get(&id) - .unwrap_or(&DatabaseGameStatus::Remote {}) - .clone(); - drop(db_handle); +pub fn fetch_game_status(id: String) -> Result { + let status = GameStatusManager::fetch_state(&id); Ok(status) } @@ -277,7 +262,7 @@ pub fn on_game_complete( let mut handle = DB.borrow_data_mut().unwrap(); handle .games - .game_versions + .versions .entry(game_id.clone()) .or_default() .insert(version_name.clone(), data.clone()); @@ -285,12 +270,12 @@ pub fn on_game_complete( DB.save().unwrap(); let status = if data.setup_command.is_empty() { - DatabaseGameStatus::Installed { + GameStatus::Installed { version_name, install_dir, } } else { - DatabaseGameStatus::SetupRequired { + GameStatus::SetupRequired { version_name, install_dir, } @@ -299,14 +284,17 @@ pub fn on_game_complete( let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .games - .games_statuses + .statuses .insert(game_id.clone(), status.clone()); drop(db_handle); DB.save().unwrap(); app_handle .emit( &format!("update_game/{}", game_id), - GameUpdateEvent { game_id, status }, + GameUpdateEvent { + game_id, + status: (Some(status), None), + }, ) .unwrap(); diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index 8eed7ef..ed58d6d 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -10,7 +10,7 @@ use log::info; use serde::{Deserialize, Serialize}; use crate::{ - db::{DatabaseGameStatus, DATA_ROOT_DIR}, + db::{GameStatus, DATA_ROOT_DIR}, DB, }; @@ -74,11 +74,11 @@ impl ProcessManager { let db_lock = DB.borrow_data().unwrap(); let game_status = db_lock .games - .games_statuses + .statuses .get(&game_id) .ok_or("Game not installed")?; - let DatabaseGameStatus::Installed { + let GameStatus::Installed { version_name, install_dir, } = game_status @@ -88,7 +88,7 @@ impl ProcessManager { let game_version = db_lock .games - .game_versions + .versions .get(&game_id) .ok_or("Invalid game ID".to_owned())? .get(version_name) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs deleted file mode 100644 index 8b13789..0000000 --- a/src-tauri/src/settings.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..9f66307 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use crate::{ + db::{GameStatus, GameTransientStatus}, + DB, +}; + +pub type GameStatusWithTransient = ( + Option, + Option, +); +pub struct GameStatusManager {} + +impl GameStatusManager { + pub fn fetch_state(game_id: &String) -> GameStatusWithTransient { + let db_lock = DB.borrow_data().unwrap(); + let offline_state = db_lock.games.statuses.get(game_id).cloned(); + let online_state = db_lock.games.transient_statuses.get(game_id).cloned(); + drop(db_lock); + + if online_state.is_some() { + return (None, online_state); + } + + if offline_state.is_some() { + return (offline_state, None); + } + + return (None, None); + } +}