feat: Resume download button

Also added DBWrite and DBRead structs to make database management easier

Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
quexeky
2025-06-21 12:51:50 +10:00
parent abf371c9fc
commit fd61903130
16 changed files with 335 additions and 123 deletions

View File

@ -103,6 +103,7 @@ const emit = defineEmits<{
(e: "uninstall"): void; (e: "uninstall"): void;
(e: "kill"): void; (e: "kill"): void;
(e: "options"): void; (e: "options"): void;
(e: "resume"): void
}>(); }>();
const showDropdown = computed( const showDropdown = computed(
@ -128,6 +129,8 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]: [GameStatusEnum.Running]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.PartiallyInstalled]:
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
}; };
const buttonNames: { [key in GameStatusEnum]: string } = { const buttonNames: { [key in GameStatusEnum]: string } = {
@ -139,6 +142,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Updating]: "Updating", [GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling", [GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop", [GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume"
}; };
const buttonIcons: { [key in GameStatusEnum]: Component } = { const buttonIcons: { [key in GameStatusEnum]: Component } = {
@ -150,6 +154,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Updating]: ArrowDownTrayIcon, [GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon, [GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon, [GameStatusEnum.Running]: PlayIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
}; };
const buttonActions: { [key in GameStatusEnum]: () => void } = { const buttonActions: { [key in GameStatusEnum]: () => void } = {
@ -161,5 +166,6 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.Updating]: () => emit("queue"), [GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {}, [GameStatusEnum.Uninstalling]: () => {},
[GameStatusEnum.Running]: () => emit("kill"), [GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
}; };
</script> </script>

View File

@ -32,6 +32,7 @@
@uninstall="() => uninstall()" @uninstall="() => uninstall()"
@kill="() => kill()" @kill="() => kill()"
@options="() => (configureModalOpen = true)" @options="() => (configureModalOpen = true)"
@resume="() => console.log('resume')"
:status="status" :status="status"
/> />
<a <a
@ -495,6 +496,7 @@ const currentImageIndex = ref(0);
const configureModalOpen = ref(false); const configureModalOpen = ref(false);
async function installFlow() { async function installFlow() {
installFlowOpen.value = true; installFlowOpen.value = true;
versionOptions.value = undefined; versionOptions.value = undefined;
@ -532,6 +534,15 @@ async function install() {
installLoading.value = false; installLoading.value = false;
} }
async function resumeDownload() {
try {
await invoke("resume_download", { game_id: game.value.id, version: status.value.version_name!, install_dir: status.value.install_dir! })
}
catch(e) {
console.error(e)
}
}
async function launch() { async function launch() {
try { try {
await invoke("launch_game", { id: game.value.id }); await invoke("launch_game", { id: game.value.id });

View File

@ -1,4 +1,4 @@
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use log::debug; use log::debug;
use tauri::AppHandle; use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt; use tauri_plugin_autostart::ManagerExt;
@ -17,7 +17,6 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
db_handle.settings.autostart = enabled; db_handle.settings.autostart = enabled;
drop(db_handle); drop(db_handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -9,7 +9,7 @@ use serde_json::Value;
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError}; use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
use super::{ use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, db::{borrow_db_checked, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings, models::data::Settings,
}; };
@ -26,8 +26,6 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
pub fn delete_download_dir(index: usize) { pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked(); let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index); lock.applications.install_dirs.remove(index);
drop(lock);
save_db();
} }
#[tauri::command] #[tauri::command]
@ -58,7 +56,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
} }
lock.applications.install_dirs.push(new_dir); lock.applications.install_dirs.push(new_dir);
drop(lock); drop(lock);
save_db();
Ok(()) Ok(())
} }
@ -72,8 +69,6 @@ pub fn update_settings(new_settings: Value) {
} }
let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings; db_lock.settings = new_settings;
drop(db_lock);
save_db();
} }
#[tauri::command] #[tauri::command]
pub fn fetch_settings() -> Settings { pub fn fetch_settings() -> Settings {

View File

@ -1,7 +1,5 @@
use std::{ use std::{
fs::{self, create_dir_all}, fs::{self, create_dir_all}, mem::{replace, ManuallyDrop}, ops::{Deref, DerefMut}, path::PathBuf, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}
path::PathBuf,
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
use chrono::Utc; use chrono::Utc;
@ -18,7 +16,6 @@ use super::models::data::Database;
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop"))); LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
// Custom JSON serializer to support everything we need // Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct DropDatabaseSerializer; pub struct DropDatabaseSerializer;
@ -27,15 +24,16 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer for DropDatabaseSerializer
{ {
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string())) native_model::rmp_serde_1_3::RmpSerde::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
} }
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> { fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new(); let mut buf = Vec::new();
s.read_to_end(&mut buf) s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?; .map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val = let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?; .map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val) Ok(val)
} }
} }
@ -122,9 +120,48 @@ fn handle_invalid_database(
PathDatabase::create_at_path(db_path, db).expect("Database could not be created") PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
} }
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> { // To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = RwLockWriteGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = RwLockReadGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Drop for DBWrite<'a> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() { match DB.borrow_data() {
Ok(data) => data, Ok(data) => DBRead(data),
Err(e) => { Err(e) => {
error!("database borrow failed with error {}", e); error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e); panic!("database borrow failed with error {}", e);
@ -132,22 +169,12 @@ pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
} }
} }
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> { pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() { match DB.borrow_data_mut() {
Ok(data) => data, Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => { Err(e) => {
error!("database borrow mut failed with error {}", e); error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e); panic!("database borrow mut failed with error {}", e);
} }
} }
} }
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}

View File

@ -1,19 +1,29 @@
use crate::database::models::data::{Database, DatabaseCompatInfo};
pub mod data { pub mod data {
use std::path::PathBuf;
use native_model::{native_model, Model}; use native_model::{native_model, Model};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion; pub type GameVersion = v1::GameVersion;
pub type Database = v2::Database; pub type Database = v3::Database;
pub type Settings = v1::Settings; pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth; pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus; pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus; pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata; pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType; pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications; pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo; pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::{collections::HashMap, process::Command};
use serde_with::serde_as;
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
pub mod v1 { pub mod v1 {
use crate::process::process_manager::Platform; use crate::process::process_manager::Platform;
use serde_with::serde_as; use serde_with::serde_as;
@ -102,7 +112,7 @@ pub mod data {
} }
// Stuff that shouldn't be synced to disk // Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus { pub enum ApplicationTransientStatus {
Downloading { version_name: String }, Downloading { version_name: String },
Uninstalling {}, Uninstalling {},
@ -164,12 +174,126 @@ pub mod data {
pub mod v2 { pub mod v2 {
use std::{collections::HashMap, path::PathBuf, process::Command}; use std::{collections::HashMap, path::PathBuf, process::Command};
use serde_with::serde_as;
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE; use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use super::*; use super::*;
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
pub umu_installed: bool,
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: crate::database::models::Database::create_new_compat_info(),
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::{collections::HashMap, path::PathBuf, process::Command};
use serde_with::serde_as;
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use super::*;
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database { pub struct Database {
#[serde(default)] #[serde(default)]
pub settings: Settings, pub settings: Settings,
@ -181,55 +305,13 @@ pub mod data {
pub cache_dir: PathBuf, pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>, pub compat_info: Option<DatabaseCompatInfo>,
} }
impl From<v2::Database> for Database {
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] fn from(value: v2::Database) -> Self {
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
umu_installed: bool,
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self { Self {
settings: value.settings, settings: value.settings,
auth: value.auth, auth: value.auth,
base_url: value.base_url, base_url: value.base_url,
applications: value.applications, applications: value.applications.into(),
prev_database: value.prev_database, prev_database: value.prev_database,
cache_dir: value.cache_dir, cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(), compat_info: Database::create_new_compat_info(),
@ -237,4 +319,37 @@ pub mod data {
} }
} }
} }
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
} }

View File

@ -37,13 +37,15 @@ pub fn fetch_game(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
offline!( let res = offline!(
state, state,
fetch_game_logic, fetch_game_logic,
fetch_game_logic_offline, fetch_game_logic_offline,
game_id, game_id,
state state
) );
println!("Res: {:?}", &res);
res
} }
#[tauri::command] #[tauri::command]

View File

@ -1,9 +1,7 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::{ use crate::{
download_manager::{ database::db::borrow_db_checked, download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable}, error::download_manager_error::DownloadManagerError, AppState
download_manager::DownloadManagerSignal, downloadable::Downloadable,
}, error::download_manager_error::DownloadManagerError, AppState
}; };
use super::download_agent::GameDownloadAgent; use super::download_agent::GameDownloadAgent;
@ -16,7 +14,7 @@ pub fn download_game(
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> { ) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender(); let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
game_id, game_id,
game_version, game_version,
install_dir, install_dir,
@ -28,3 +26,18 @@ pub fn download_game(
.download_manager .download_manager
.queue_download(game_download_agent)?) .queue_download(game_download_agent)?)
} }
#[tauri::command]
pub fn resume_download(
game_id: String,
version: String,
install_dir: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id, version, install_dir.into(), sender
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state.lock().unwrap().download_manager.queue_download(game_download_agent)?)
}

View File

@ -1,5 +1,5 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::database::db::borrow_db_checked; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{ use crate::database::models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus, ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
}; };
@ -19,11 +19,11 @@ use log::{debug, error, info};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use slice_deque::SliceDeque; use slice_deque::SliceDeque;
use std::fs::{create_dir_all, File, OpenOptions}; use std::fs::{create_dir_all, File, OpenOptions};
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter, Manager};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use rustix::fs::{fallocate, FallocateFlags}; use rustix::fs::{fallocate, FallocateFlags};
@ -45,19 +45,27 @@ pub struct GameDownloadAgent {
} }
impl GameDownloadAgent { impl GameDownloadAgent {
pub fn new( pub fn new_from_index(
id: String, id: String,
version: String, version: String,
target_download_dir: usize, target_download_dir: usize,
sender: Sender<DownloadManagerSignal>, sender: Sender<DownloadManagerSignal>,
) -> Self { ) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock); drop(db_lock);
Self::new(id, version, base_dir, sender)
}
pub fn new(
id: String,
version: String,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let base_dir_path = Path::new(&base_dir); let base_dir_path = Path::new(&base_dir);
let data_base_dir_path = base_dir_path.join(id.clone()); let data_base_dir_path = base_dir_path.join(id.clone());
@ -204,7 +212,12 @@ impl GameDownloadAgent {
let container = path.parent().unwrap(); let container = path.parent().unwrap();
create_dir_all(container).unwrap(); create_dir_all(container).unwrap();
let file = OpenOptions::new().read(true).write(true).create(true).open(path.clone()).unwrap(); let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path.clone())
.unwrap();
let mut running_offset = 0; let mut running_offset = 0;
for (index, length) in chunk.lengths.iter().enumerate() { for (index, length) in chunk.lengths.iter().enumerate() {
@ -260,7 +273,12 @@ impl GameDownloadAgent {
let progress_handle = ProgressHandle::new(progress, self.progress.clone()); let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it // If we've done this one already, skip it
if self.completed_contexts.lock().unwrap().contains(&context.checksum) { if self
.completed_contexts
.lock()
.unwrap()
.contains(&context.checksum)
{
progress_handle.skip(context.length); progress_handle.skip(context.length);
continue; continue;
} }
@ -384,7 +402,7 @@ impl Downloadable for GameDownloadAgent {
error!("error while managing download: {}", error); error!("error while managing download: {}", error);
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
handle handle
.applications .applications
.transient_statuses .transient_statuses
@ -404,12 +422,41 @@ impl Downloadable for GameDownloadAgent {
fn on_incomplete(&self, app_handle: &tauri::AppHandle) { fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata(); let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued; *self.status.lock().unwrap() = DownloadStatus::Queued;
borrow_db_mut_checked().applications.game_statuses.insert(
self.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: self.version.clone(),
install_dir: self.stored_manifest.base_path.to_string_lossy().to_string(),
},
);
borrow_db_mut_checked()
.applications
.installed_game_version
.insert(self.id.clone(), self.metadata());
borrow_db_mut_checked().applications.game_statuses.insert(
self.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: self.version.clone(),
install_dir: self.stored_manifest.base_path.to_string_lossy().to_string(),
},
);
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
GameUpdateEvent { GameUpdateEvent {
game_id: meta.id.clone(), game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None), status: (
Some(GameDownloadStatus::PartiallyInstalled {
version_name: self.version.clone(),
install_dir: self
.stored_manifest
.base_path
.to_string_lossy()
.to_string(),
}),
None,
),
version: None, version: None,
}, },
) )

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use tauri::Emitter; use tauri::Emitter;
use tauri::AppHandle; use tauri::AppHandle;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{ use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion, ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
}; };
@ -18,9 +18,9 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header; use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request; use crate::remote::requests::make_request;
use crate::{AppState, DB}; use crate::AppState;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
@ -144,7 +144,7 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap(); let mut state_handle = state.lock().unwrap();
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
@ -220,7 +220,7 @@ pub fn fetch_game_logic_offline(
id: String, id: String,
_state: tauri::State<'_, Mutex<AppState>>, _state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
None => None, None => None,
@ -341,7 +341,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.entry(meta.id.clone()) .entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {}); .and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle); drop(db_handle);
save_db();
debug!("uninstalled game id {}", &meta.id); debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", {}).unwrap(); app_handle.emit("update_library", {}).unwrap();
@ -404,7 +403,6 @@ pub fn on_game_complete(
.insert(meta.id.clone(), meta.clone()); .insert(meta.id.clone(), meta.clone());
drop(handle); drop(handle);
save_db();
let status = if game_version.setup_command.is_empty() { let status = if game_version.setup_command.is_empty() {
GameDownloadStatus::Installed { GameDownloadStatus::Installed {
@ -424,7 +422,6 @@ pub fn on_game_complete(
.game_statuses .game_statuses
.insert(meta.id.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
save_db();
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
@ -468,7 +465,7 @@ pub fn update_game_configuration(
game_id: String, game_id: String,
options: FrontendGameOptions, options: FrontendGameOptions,
) -> Result<(), LibraryError> { ) -> Result<(), LibraryError> {
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
let installed_version = handle let installed_version = handle
.applications .applications
.installed_game_version .installed_game_version
@ -499,8 +496,6 @@ pub fn update_game_configuration(
.unwrap() .unwrap()
.insert(version.to_string(), existing_configuration); .insert(version.to_string(), existing_configuration);
drop(handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -7,7 +7,7 @@ mod error;
mod process; mod process;
mod remote; mod remote;
use crate::database::db::DatabaseImpls; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use client::{ use client::{
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart}, autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
cleanup::{cleanup_and_exit, quit}, cleanup::{cleanup_and_exit, quit},
@ -156,6 +156,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
for (game_id, status) in statuses.into_iter() { for (game_id, status) in statuses.into_iter() {
match status { match status {
GameDownloadStatus::Remote {} => {} GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {},
GameDownloadStatus::SetupRequired { GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir, install_dir,
@ -173,7 +174,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
if !install_dir_path.exists() { if !install_dir_path.exists() {
missing_games.push(game_id); missing_games.push(game_id);
} }
} },
} }
} }
@ -259,6 +260,7 @@ pub fn run() {
delete_game_in_collection, delete_game_in_collection,
// Downloads // Downloads
download_game, download_game,
resume_download,
move_download_in_queue, move_download_in_queue,
pause_downloads, pause_downloads,
resume_downloads, resume_downloads,

View File

@ -193,6 +193,9 @@ impl ProcessManager<'_> {
version_name, version_name,
install_dir, install_dir,
} => (version_name, install_dir), } => (version_name, install_dir),
GameDownloadStatus::PartiallyInstalled {
version_name, install_dir
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded), _ => return Err(ProcessError::NotDownloaded),
}; };
@ -248,7 +251,11 @@ impl ProcessManager<'_> {
version_name: _, version_name: _,
install_dir: _, install_dir: _,
} => (&game_version.setup_command, &game_version.setup_args), } => (&game_version.setup_command, &game_version.setup_args),
GameDownloadStatus::Remote {} => unreachable!("nuh uh"), GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir
} => unreachable!("Game registered as 'Partially Installed'"),
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
}; };
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch); let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);

View File

@ -10,7 +10,7 @@ use url::Url;
use crate::{ use crate::{
database::{ database::{
db::{borrow_db_checked, borrow_db_mut_checked, save_db}, db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth, models::data::DatabaseAuth,
}, },
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
@ -122,8 +122,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
client_id: response_struct.id, client_id: response_struct.id,
web_token: None, // gets created later web_token: None, // gets created later
}); });
drop(handle);
save_db();
} }
let web_token = { let web_token = {
@ -140,8 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
let mut_auth = handle.auth.as_mut().unwrap(); let mut_auth = handle.auth.as_mut().unwrap();
mut_auth.web_token = Some(web_token); mut_auth.web_token = Some(web_token);
drop(handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -6,7 +6,7 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request}, remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus, AppState, AppStatus,
@ -65,8 +65,6 @@ pub fn sign_out(app: AppHandle) {
{ {
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
handle.auth = None; handle.auth = None;
drop(handle);
save_db();
} }
// Update app state // Update app state

View File

@ -5,7 +5,7 @@ use serde::Deserialize;
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_mut_checked, save_db}, database::db::{borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
AppState, AppStatus, AppState, AppStatus,
}; };
@ -40,9 +40,6 @@ pub fn use_remote_logic(
let mut db_state = borrow_db_mut_checked(); let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string(); db_state.base_url = base_url.to_string();
drop(db_state);
save_db();
Ok(()) Ok(())
} }

View File

@ -59,11 +59,13 @@ export enum GameStatusEnum {
Uninstalling = "Uninstalling", Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired", SetupRequired = "SetupRequired",
Running = "Running", Running = "Running",
PartiallyInstalled = "PartiallyInstalled"
} }
export type GameStatus = { export type GameStatus = {
type: GameStatusEnum; type: GameStatusEnum;
version_name?: string; version_name?: string;
install_dir?: string;
}; };
export enum DownloadableType { export enum DownloadableType {