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;
+};