feat(download manager): Added generic download manager

Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
quexeky
2025-01-04 15:47:14 +11:00
parent 8be1dd435c
commit 6159319172
20 changed files with 363 additions and 202 deletions

View File

@ -1,7 +1,8 @@
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import type { DownloadableMetadata } from "~/types";
export type QueueState = { export type QueueState = {
queue: Array<{ id: string; status: string; progress: number | null }>; queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
status: string; status: string;
}; };

View File

@ -13,6 +13,7 @@ export type SerializedGameStatus = [
]; ];
export const parseStatus = (status: SerializedGameStatus): GameStatus => { export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) { if (status[0]) {
return { return {
type: status[0].type, type: status[0].type,
@ -28,28 +29,29 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => {
} }
}; };
export const useGame = async (id: string) => { export const useGame = async (gameId: string) => {
if (!gameRegistry[id]) { if (!gameRegistry[gameId]) {
const data: { game: Game; status: SerializedGameStatus } = await invoke( const data: { game: Game; status: SerializedGameStatus } = await invoke(
"fetch_game", "fetch_game",
{ {
id, gameId,
} }
); );
gameRegistry[id] = data.game; gameRegistry[gameId] = data.game;
if (!gameStatusRegistry[id]) { if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[id] = ref(parseStatus(data.status)); gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${id}`, (event) => { listen(`update_game/${gameId}`, (event) => {
const payload: { const payload: {
status: SerializedGameStatus; status: SerializedGameStatus;
} = event.payload as any; } = event.payload as any;
gameStatusRegistry[id].value = parseStatus(payload.status); console.log(payload.status);
gameStatusRegistry[gameId].value = parseStatus(payload.status);
}); });
} }
} }
const game = gameRegistry[id]; const game = gameRegistry[gameId];
const status = gameStatusRegistry[id]; const status = gameStatusRegistry[gameId];
return { game, status }; return { game, status };
}; };

View File

@ -14,19 +14,19 @@
</div> </div>
<draggable v-model="queue.queue" @end="onEnd"> <draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }"> <template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<li v-if="games[element.id]" :key="element.id" <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"> 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"> <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.id].cover" alt="" /> <img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" />
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="text-xl font-semibold text-zinc-100"> <p class="text-xl font-semibold text-zinc-100">
<NuxtLink :href="`/library/${element.id}`" class=""> <NuxtLink :href="`/library/${element.meta.id}`" class="">
<span class="absolute inset-x-0 -top-px bottom-0" /> <span class="absolute inset-x-0 -top-px bottom-0" />
{{ games[element.id].game.mName }} {{ games[element.meta.id].game.mName }}
</NuxtLink> </NuxtLink>
</p> </p>
<p class="mt-1 flex text-xs/5 text-gray-500"> <p class="mt-1 flex text-xs/5 text-gray-500">
{{ games[element.id].game.mShortDescription }} {{ games[element.meta.id].game.mShortDescription }}
</p> </p>
</div> </div>
</div> </div>
@ -39,7 +39,7 @@
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" /> <div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
</div> </div>
</div> </div>
<button @click="() => cancelGame(element.id)" class="group"> <button @click="() => cancelGame(element.meta)" class="group">
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300" <XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true" /> aria-hidden="true" />
</button> </button>
@ -57,7 +57,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { XMarkIcon } from "@heroicons/vue/20/solid"; import { XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { Game, GameStatus } from "~/types"; import type { DownloadableMetadata, Game, GameStatus } from "~/types";
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener('resize', (event) => { window.addEventListener('resize', (event) => {
@ -81,7 +81,7 @@ function resetHistoryGraph() {
stats.value = { time: 0, speed: 0 }; stats.value = { time: 0, speed: 0 };
} }
function checkReset(v: QueueState) { function checkReset(v: QueueState) {
const currentGame = v.queue.at(0); const currentGame = v.queue.at(0)?.meta.id;
// If we're finished // If we're finished
if (!currentGame && previousGameId.value) { if (!currentGame && previousGameId.value) {
previousGameId.value = undefined; previousGameId.value = undefined;
@ -92,14 +92,14 @@ function checkReset(v: QueueState) {
if (!currentGame) return; if (!currentGame) return;
// If we started a new download // If we started a new download
if (currentGame && !previousGameId.value) { if (currentGame && !previousGameId.value) {
previousGameId.value = currentGame.id; previousGameId.value = currentGame;
resetHistoryGraph(); resetHistoryGraph();
return; return;
} }
// If it's a different game now // If it's a different game now
if (currentGame.id != previousGameId.value if (currentGame != previousGameId.value
) { ) {
previousGameId.value = currentGame.id; previousGameId.value = currentGame;
resetHistoryGraph(); resetHistoryGraph();
return; return;
} }
@ -118,7 +118,7 @@ watch(stats, (v) => {
}) })
function loadGamesForQueue(v: typeof queue.value) { function loadGamesForQueue(v: typeof queue.value) {
for (const { id } of v.queue) { for (const { meta: { id } } of v.queue) {
if (games.value[id]) return; if (games.value[id]) return;
(async () => { (async () => {
const gameData = await useGame(id); const gameData = await useGame(id);
@ -137,8 +137,8 @@ async function onEnd(event: { oldIndex: number; newIndex: number }) {
}); });
} }
async function cancelGame(id: string) { async function cancelGame(meta: DownloadableMetadata) {
await invoke("cancel_game", { gameId: id }); await invoke("cancel_game", { meta });
} }
function formatKilobytes(bytes: number): string { function formatKilobytes(bytes: number): string {

9
src-tauri/Cargo.lock generated
View File

@ -1018,6 +1018,7 @@ dependencies = [
"serde", "serde",
"serde-binary", "serde-binary",
"serde_json", "serde_json",
"serde_with",
"shared_child", "shared_child",
"tauri", "tauri",
"tauri-build", "tauri-build",
@ -3856,9 +3857,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.11.0" version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@ -3874,9 +3875,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.11.0" version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",

View File

@ -45,6 +45,7 @@ boxcar = "0.2.7"
umu-wrapper-lib = "0.1.0" umu-wrapper-lib = "0.1.0"
tauri-plugin-autostart = "2.0.0" tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1" shared_child = "1.0.1"
serde_with = "3.12.0"
[dependencies.tauri] [dependencies.tauri]
version = "2.1.1" version = "2.1.1"

View File

@ -9,10 +9,11 @@ use directories::BaseDirs;
use log::debug; use log::debug;
use rustbreak::{DeSerError, DeSerializer, PathDatabase}; use rustbreak::{DeSerError, DeSerializer, PathDatabase};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::serde_as;
use tauri::AppHandle; use tauri::AppHandle;
use url::Url; use url::Url;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, library::push_game_update, process::process_manager::Platform, state::DownloadStatusManager, DB}; use crate::{download_manager::downloadable_metadata::DownloadableMetadata, library::push_game_update, process::process_manager::Platform, state::GameStatusManager, DB};
#[derive(serde::Serialize, Clone, Deserialize)] #[derive(serde::Serialize, Clone, Deserialize)]
pub struct DatabaseAuth { pub struct DatabaseAuth {
@ -24,7 +25,7 @@ pub struct DatabaseAuth {
// Strings are version names for a particular game // Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)] #[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ApplicationStatus { pub enum GameDownloadStatus {
Remote {}, Remote {},
SetupRequired { SetupRequired {
version_name: String, version_name: String,
@ -45,9 +46,9 @@ pub enum ApplicationTransientStatus {
Running {}, Running {},
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApplicationVersion { pub struct GameVersion {
pub version_index: usize, pub version_index: usize,
pub version_name: String, pub version_name: String,
pub launch_command: String, pub launch_command: String,
@ -55,13 +56,15 @@ pub struct ApplicationVersion {
pub platform: Platform, pub platform: Platform,
} }
#[serde_as]
#[derive(Serialize, Clone, Deserialize)] #[derive(Serialize, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DatabaseApplications { pub struct DatabaseApplications {
pub install_dirs: Vec<String>, pub install_dirs: Vec<String>,
// Guaranteed to exist if the game also exists in the app state map // Guaranteed to exist if the game also exists in the app state map
pub statuses: HashMap<DownloadableMetadata, ApplicationStatus>, pub game_statuses: HashMap<String, GameDownloadStatus>,
pub versions: HashMap<DownloadableMetadata, HashMap<String, ApplicationVersion>>, pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)] #[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>, pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
@ -140,9 +143,10 @@ impl DatabaseImpls for DatabaseInterface {
base_url: "".to_string(), base_url: "".to_string(),
applications: DatabaseApplications { applications: DatabaseApplications {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
statuses: HashMap::new(), game_statuses: HashMap::new(),
transient_statuses: HashMap::new(), transient_statuses: HashMap::new(),
versions: HashMap::new(), game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
}, },
}; };
debug!( debug!(
@ -222,15 +226,15 @@ pub fn fetch_download_dir_stats() -> Result<Vec<String>, String> {
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>( pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
app_handle: &AppHandle, app_handle: &AppHandle,
id: DownloadableMetadata, meta: DownloadableMetadata,
setter: F, setter: F,
) { ) {
let mut db_handle = DB.borrow_data_mut().unwrap(); let mut db_handle = DB.borrow_data_mut().unwrap();
setter(&mut db_handle, &id); setter(&mut db_handle, &meta);
drop(db_handle); drop(db_handle);
DB.save().unwrap(); DB.save().unwrap();
let status = DownloadStatusManager::fetch_state(&id); let status = GameStatusManager::fetch_state(&meta.id);
push_game_update(app_handle, id, status); push_game_update(app_handle, &meta, status);
} }

View File

@ -130,7 +130,7 @@ impl DownloadManager {
} }
pub fn cancel(&self, meta: DownloadableMetadata) { pub fn cancel(&self, meta: DownloadableMetadata) {
self.command_sender self.command_sender
.send(DownloadManagerSignal::Remove(meta)) .send(DownloadManagerSignal::Cancel(meta))
.unwrap(); .unwrap();
} }
pub fn rearrange(&self, current_index: usize, new_index: usize) { pub fn rearrange(&self, current_index: usize, new_index: usize) {

View File

@ -117,6 +117,7 @@ impl DownloadManagerBuilder {
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); let mut download_thread_lock = self.current_download_thread.lock().unwrap();
*download_thread_lock = None; *download_thread_lock = None;
drop(download_thread_lock); drop(download_thread_lock);
} }
fn stop_and_wait_current_download(&self) { fn stop_and_wait_current_download(&self) {
@ -179,12 +180,13 @@ impl DownloadManagerBuilder {
info!("Got signal Queue"); info!("Got signal Queue");
let meta = download_agent.metadata(); let meta = download_agent.metadata();
info!("Meta: {:?}", meta);
if self.download_queue.exists(meta.clone()) { if self.download_queue.exists(meta.clone()) {
info!("Download with same ID already exists"); info!("Download with same ID already exists");
return; return;
} }
let download_agent = generate_downloadable(meta.clone());
download_agent.on_initialised(&self.app_handle); download_agent.on_initialised(&self.app_handle);
self.download_queue.append(meta.clone()); self.download_queue.append(meta.clone());
self.download_agent_registry.insert(meta, download_agent); self.download_agent_registry.insert(meta, download_agent);
@ -194,9 +196,15 @@ impl DownloadManagerBuilder {
fn manage_go_signal(&mut self) { fn manage_go_signal(&mut self) {
info!("Got signal Go"); info!("Got signal Go");
if !(self.download_agent_registry.is_empty()) { return; } if self.download_agent_registry.is_empty() {
info!("Download agent registry: {:?}", self.download_agent_registry.len());
return;
}
if self.current_download_agent.is_some() { return; } if self.current_download_agent.is_some() {
info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata());
return;
}
info!("Current download queue: {:?}", self.download_queue.read()); info!("Current download queue: {:?}", self.download_queue.read());
@ -220,11 +228,11 @@ impl DownloadManagerBuilder {
let app_handle = self.app_handle.clone(); let app_handle = self.app_handle.clone();
*download_thread_lock = Some(spawn(move || { *download_thread_lock = Some(spawn(move || {
match download_agent.download() { match download_agent.download(&app_handle) {
// Ok(true) is for completed and exited properly // Ok(true) is for completed and exited properly
Ok(true) => { Ok(true) => {
download_agent.on_complete(&app_handle); download_agent.on_complete(&app_handle);
sender.send(DownloadManagerSignal::Completed(download_agent.metadata())); sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap();
}, },
// Ok(false) is for incomplete but exited properly // Ok(false) is for incomplete but exited properly
Ok(false) => { Ok(false) => {
@ -257,6 +265,7 @@ impl DownloadManagerBuilder {
self.remove_and_cleanup_front_download(&meta); self.remove_and_cleanup_front_download(&meta);
} }
} }
self.push_ui_queue_update();
self.sender.send(DownloadManagerSignal::Go).unwrap(); self.sender.send(DownloadManagerSignal::Go).unwrap();
} }
fn manage_error_signal(&mut self, error: ApplicationDownloadError) { fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
@ -279,19 +288,38 @@ impl DownloadManagerBuilder {
current_download.on_cancelled(&self.app_handle); current_download.on_cancelled(&self.app_handle);
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
self.download_queue.pop_front();
self.cleanup_current_download(); self.cleanup_current_download();
info!("Current donwload queue: {:?}", self.download_queue.read());
}
// TODO: Collapse these two into a single if statement somehow
else {
if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
}
}
} }
} }
else { else {
if let Some(download_agent) = self.download_agent_registry.get(meta) { if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
let index = self.download_queue.get_by_meta(meta); let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index { if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle); download_agent.on_cancelled(&self.app_handle);
let queue_handle = self.download_queue.edit().remove(index); let _ = self.download_queue.edit().remove(index).unwrap();
self.download_agent_registry.remove(meta); let removed = self.download_agent_registry.remove(meta);
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
} }
} }
} }
self.push_ui_queue_update();
} }
fn uninstall_application(&mut self, meta: &DownloadableMetadata) { fn uninstall_application(&mut self, meta: &DownloadableMetadata) {
let download_agent = match self.download_agent_registry.get(meta) { let download_agent = match self.download_agent_registry.get(meta) {
@ -304,24 +332,25 @@ impl DownloadManagerBuilder {
fn push_ui_stats_update(&self, kbs: usize, time: usize) { fn push_ui_stats_update(&self, kbs: usize, time: usize) {
let event_data = StatsUpdateEvent { speed: kbs, time }; let event_data = StatsUpdateEvent { speed: kbs, time };
self.app_handle.emit("update_stats", event_data); self.app_handle.emit("update_stats", event_data).unwrap();
} }
fn push_ui_queue_update(&self) { fn push_ui_queue_update(&self) {
let registry = &self.download_agent_registry; let queue = &self.download_queue.read();
let queue_objs = registry let queue_objs = queue
.iter() .iter()
.map(|(key, val)| QueueUpdateEventQueueData { .map(|(key)| {
meta: DownloadableMetadata::clone(&key), let val = self.download_agent_registry.get(key).unwrap();
status: val.status(), QueueUpdateEventQueueData {
progress: val.progress().get_progress() meta: DownloadableMetadata::clone(&key),
}) status: val.status(),
progress: val.progress().get_progress()
}})
.collect(); .collect();
let event_data = QueueUpdateEvent { let event_data = QueueUpdateEvent {
queue: queue_objs, queue: queue_objs,
status: self.status.lock().unwrap().clone(),
}; };
self.app_handle.emit("update_queue", event_data); self.app_handle.emit("update_queue", event_data).unwrap();
} }
} }
/* /*

View File

@ -1,4 +1,4 @@
use std::sync::{mpsc::Sender, Arc}; use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}};
use tauri::AppHandle; use tauri::AppHandle;
@ -7,7 +7,7 @@ use super::{
}; };
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
fn download(&self) -> Result<bool, ApplicationDownloadError>; fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>; fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl; fn control_flag(&self) -> DownloadThreadControl;
fn status(&self) -> DownloadStatus; fn status(&self) -> DownloadStatus;

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
pub enum DownloadType { pub enum DownloadType {
Game, Game,
Tool, Tool,
@ -9,13 +9,14 @@ pub enum DownloadType {
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata { pub struct DownloadableMetadata {
pub id: String, pub id: String,
pub version: String, pub version: Option<String>,
pub download_type: DownloadType pub download_type: DownloadType
} }
impl DownloadableMetadata { impl DownloadableMetadata {
pub fn new(id: String, version: String, download_type: DownloadType) -> Self { pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self { Self {
id, id,
version, version,

View File

@ -1,22 +1,24 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::db::{set_game_status, DatabaseImpls}; use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
use crate::download_manager::application_download_error::ApplicationDownloadError; use crate::download_manager::application_download_error::ApplicationDownloadError;
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
use crate::download_manager::downloadable::Downloadable; use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::downloadable_metadata::DownloadableMetadata; use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject}; use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::library::{on_game_complete, push_game_update};
use crate::remote::RemoteAccessError; use crate::remote::RemoteAccessError;
use crate::DB; use crate::DB;
use log::{debug, error, info}; use log::{debug, error, info};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use tauri::Emitter; use tauri::{AppHandle, Emitter};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, remove_dir_all, File};
use std::path::Path; use std::path::Path;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::spawn;
use std::time::Instant; use std::time::Instant;
use urlencoding::encode; use urlencoding::encode;
@ -36,10 +38,9 @@ pub struct GameDownloadAgent {
pub progress: Arc<ProgressObject>, pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>, sender: Sender<DownloadManagerSignal>,
pub stored_manifest: StoredManifest, pub stored_manifest: StoredManifest,
status: Mutex<DownloadStatus>
} }
impl GameDownloadAgent { impl GameDownloadAgent {
pub fn new( pub fn new(
id: String, id: String,
@ -70,6 +71,7 @@ impl GameDownloadAgent {
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender, sender,
stored_manifest, stored_manifest,
status: Mutex::new(DownloadStatus::Queued),
} }
} }
@ -87,10 +89,14 @@ impl GameDownloadAgent {
} }
// Blocking // Blocking
pub fn download(&self) -> Result<bool, ApplicationDownloadError> { pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
info!("Setting up download");
self.setup_download()?; self.setup_download()?;
info!("Setting progress object params");
self.set_progress_object_params(); self.set_progress_object_params();
info!("Running");
let timer = Instant::now(); let timer = Instant::now();
push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError); let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
info!( info!(
@ -242,6 +248,7 @@ impl GameDownloadAgent {
pool.scope(|scope| { pool.scope(|scope| {
for (index, context) in self.contexts.lock().unwrap().iter().enumerate() { for (index, context) in self.contexts.lock().unwrap().iter().enumerate() {
info!("Running index {}", index);
let completed_indexes = completed_indexes_loop_arc.clone(); let completed_indexes = completed_indexes_loop_arc.clone();
let progress = self.progress.get(index); // Clone arcs let progress = self.progress.get(index); // Clone arcs
@ -305,8 +312,9 @@ impl GameDownloadAgent {
} }
impl Downloadable for GameDownloadAgent { impl Downloadable for GameDownloadAgent {
fn download(&self) -> Result<bool, ApplicationDownloadError> { fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.download() *self.status.lock().unwrap() = DownloadStatus::Downloading;
self.download(app_handle)
} }
fn progress(&self) -> Arc<ProgressObject> { fn progress(&self) -> Arc<ProgressObject> {
@ -318,14 +326,20 @@ impl Downloadable for GameDownloadAgent {
} }
fn metadata(&self) -> DownloadableMetadata { fn metadata(&self) -> DownloadableMetadata {
todo!() DownloadableMetadata {
id: self.id.clone(),
version: Some(self.version.clone()),
download_type: DownloadType::Game,
}
} }
fn on_initialised(&self, _app_handle: &tauri::AppHandle) { fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
*self.status.lock().unwrap() = DownloadStatus::Queued;
return; return;
} }
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) { fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
*self.status.lock().unwrap() = DownloadStatus::Error;
app_handle app_handle
.emit("download_error", error.to_string()) .emit("download_error", error.to_string())
.unwrap(); .unwrap();
@ -339,22 +353,87 @@ impl Downloadable for GameDownloadAgent {
} }
fn on_complete(&self, app_handle: &tauri::AppHandle) { fn on_complete(&self, app_handle: &tauri::AppHandle) {
todo!() on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap();
} }
fn on_incomplete(&self, app_handle: &tauri::AppHandle) { fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
todo!() *self.status.lock().unwrap() = DownloadStatus::Queued;
return;
} }
fn on_cancelled(&self, app_handle: &tauri::AppHandle) { fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
todo!() return;
} }
fn on_uninstall(&self, app_handle: &tauri::AppHandle) { fn on_uninstall(&self, app_handle: &tauri::AppHandle) {
todo!() let mut db_handle = DB.borrow_data_mut().unwrap();
let metadata = self.metadata();
db_handle
.applications
.transient_statuses
.entry(metadata.clone())
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
push_game_update(
app_handle,
&metadata,
(None, Some(ApplicationTransientStatus::Uninstalling {})),
);
let previous_state = db_handle.applications.game_statuses.get(&metadata.id).cloned();
if previous_state.is_none() {
info!("uninstall job doesn't have previous state, failing silently");
return;
}
let previous_state = previous_state.unwrap();
if let Some((version_name, install_dir)) = match previous_state {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} {
db_handle
.applications
.transient_statuses
.entry(metadata.clone())
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
drop(db_handle);
let sender = self.sender.clone();
let app_handle = app_handle.clone();
spawn(move || match remove_dir_all(install_dir) {
Err(e) => {
sender
.send(DownloadManagerSignal::Error(ApplicationDownloadError::IoError(
e.kind(),
)))
.unwrap();
}
Ok(_) => {
let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle.applications.transient_statuses.remove(&metadata);
db_handle
.applications
.game_statuses
.entry(metadata.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
DB.save().unwrap();
info!("uninstalled game id {}", metadata.id);
push_game_update(&app_handle, &metadata, (Some(GameDownloadStatus::Remote {}), None));
}
});
}
} }
fn status(&self) -> DownloadStatus { fn status(&self) -> DownloadStatus {
todo!() self.status.lock().unwrap().clone()
} }
} }

View File

@ -47,8 +47,8 @@ pub fn move_game_in_queue(
} }
#[tauri::command] #[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, game_id: DownloadableMetadata) { pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(Arc::new(game_id)) state.lock().unwrap().download_manager.cancel(meta)
} }
/* /*

View File

@ -22,7 +22,7 @@ use auth::{
}; };
use cleanup::{cleanup_and_exit, quit}; use cleanup::{cleanup_and_exit, quit};
use db::{ use db::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, ApplicationStatus, add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus,
DATA_ROOT_DIR, DATA_ROOT_DIR,
}; };
use download_manager::download_manager::DownloadManager; use download_manager::download_manager::DownloadManager;
@ -33,7 +33,7 @@ use downloads::download_commands::*;
use http::Response; use http::Response;
use http::{header::*, response::Builder as ResponseBuilder}; use http::{header::*, response::Builder as ResponseBuilder};
use library::{ use library::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game, fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game
}; };
use log::{debug, info, warn, LevelFilter}; use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
@ -82,7 +82,7 @@ pub struct User {
pub struct AppState<'a> { pub struct AppState<'a> {
status: AppStatus, status: AppStatus,
user: Option<User>, user: Option<User>,
games: HashMap<DownloadableMetadata, Game>, games: HashMap<String, Game>,
#[serde(skip_serializing)] #[serde(skip_serializing)]
download_manager: Arc<DownloadManager>, download_manager: Arc<DownloadManager>,
@ -149,12 +149,12 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let db_handle = DB.borrow_data().unwrap(); let db_handle = DB.borrow_data().unwrap();
let mut missing_games = Vec::new(); let mut missing_games = Vec::new();
let statuses = db_handle.applications.statuses.clone(); let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle); drop(db_handle);
for (game_id, status) in statuses.into_iter() { for (game_id, status) in statuses.into_iter() {
match status { match status {
db::ApplicationStatus::Remote {} => {} db::GameDownloadStatus::Remote {} => {}
db::ApplicationStatus::SetupRequired { db::GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -163,7 +163,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id); missing_games.push(game_id);
} }
} }
db::ApplicationStatus::Installed { db::GameDownloadStatus::Installed {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -181,9 +181,9 @@ fn setup(handle: AppHandle) -> AppState<'static> {
for game_id in missing_games { for game_id in missing_games {
db_handle db_handle
.applications .applications
.statuses .game_statuses
.entry(game_id) .entry(game_id)
.and_modify(|v| *v = ApplicationStatus::Remote {}); .and_modify(|v| *v = GameDownloadStatus::Remote {});
} }
drop(db_handle); drop(db_handle);
info!("finished setup!"); info!("finished setup!");

View File

@ -1,4 +1,4 @@
use std::sync::{Arc, Mutex}; use std::sync::Mutex;
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -6,12 +6,13 @@ use tauri::Emitter;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use urlencoding::encode; use urlencoding::encode;
use crate::db::DatabaseImpls; use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::db::ApplicationVersion; use crate::db::GameVersion;
use crate::db::ApplicationStatus; use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::download_manager::{DownloadManagerStatus, DownloadStatus};
use crate::download_manager::downloadable_metadata::DownloadableMetadata; use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::process::process_manager::Platform; use crate::process::process_manager::Platform;
use crate::remote::RemoteAccessError;
use crate::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::{DropServerError, RemoteAccessError}; use crate::remote::{DropServerError, RemoteAccessError};
use crate::state::{GameStatusManager, GameStatusWithTransient}; use crate::state::{GameStatusManager, GameStatusWithTransient};
use crate::{auth::generate_authorization_header, AppState, DB}; use crate::{auth::generate_authorization_header, AppState, DB};
@ -20,12 +21,13 @@ use crate::{auth::generate_authorization_header, AppState, DB};
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
status: GameStatusWithTransient,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Game { pub struct Game {
meta: DownloadableMetadata, id: String,
m_name: String, m_name: String,
m_short_description: String, m_short_description: String,
m_description: String, m_description: String,
@ -39,7 +41,7 @@ pub struct Game {
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent { pub struct GameUpdateEvent {
pub game_id: String, pub game_id: String,
pub status: GameStatusWithTransient, pub status: (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>),
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@ -52,7 +54,6 @@ pub struct QueueUpdateEventQueueData {
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
pub struct QueueUpdateEvent { pub struct QueueUpdateEvent {
pub queue: Vec<QueueUpdateEventQueueData>, pub queue: Vec<QueueUpdateEventQueueData>,
pub status: DownloadManagerStatus,
} }
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
@ -99,12 +100,12 @@ fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let mut db_handle = DB.borrow_data_mut().unwrap(); let mut db_handle = DB.borrow_data_mut().unwrap();
for game in games.iter() { for game in games.iter() {
handle.games.insert(game.meta.clone(), game.clone()); handle.games.insert(game.id.clone(), game.clone());
if !db_handle.applications.statuses.contains_key(&game.meta) { if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle db_handle
.applications .applications
.statuses .game_statuses
.insert(game.meta.clone(), ApplicationStatus::Remote {}); .insert(game.id.clone(), GameDownloadStatus::Remote {});
} }
} }
@ -119,15 +120,15 @@ pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, String> {
} }
fn fetch_game_logic( fn fetch_game_logic(
meta: DownloadableMetadata, id: String,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let state = app.state::<Mutex<AppState>>(); let state = app.state::<Mutex<AppState>>();
let mut state_handle = state.lock().unwrap(); let mut state_handle = state.lock().unwrap();
let game = state_handle.games.get(&meta); let game = state_handle.games.get(&id);
if let Some(game) = game { if let Some(game) = game {
let status = DownloadStatusManager::fetch_state(&meta); let status = GameStatusManager::fetch_state(&id);
let data = FetchGameStruct { let data = FetchGameStruct {
game: game.clone(), game: game.clone(),
@ -139,7 +140,7 @@ fn fetch_game_logic(
let base_url = DB.fetch_base_url(); let base_url = DB.fetch_base_url();
let endpoint = base_url.join(&format!("/api/v1/game/{}", meta.id))?; let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?;
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
@ -158,18 +159,18 @@ fn fetch_game_logic(
} }
let game = response.json::<Game>()?; let game = response.json::<Game>()?;
state_handle.games.insert(meta.clone(), game.clone()); state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = DB.borrow_data_mut().unwrap(); let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle db_handle
.applications .applications
.statuses .game_statuses
.entry(meta.clone()) .entry(id.clone())
.or_insert(ApplicationStatus::Remote {}); .or_insert(GameDownloadStatus::Remote {});
drop(db_handle); drop(db_handle);
let status = DownloadStatusManager::fetch_state(&meta); let status = GameStatusManager::fetch_state(&id);
let data = FetchGameStruct { let data = FetchGameStruct {
game: game.clone(), game: game.clone(),
@ -180,8 +181,8 @@ fn fetch_game_logic(
} }
#[tauri::command] #[tauri::command]
pub fn fetch_game(id: DownloadableMetadata, app: tauri::AppHandle) -> Result<FetchGameStruct, String> { pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
let result = fetch_game_logic(id, app); let result = fetch_game_logic(game_id, app);
if result.is_err() { if result.is_err() {
return Err(result.err().unwrap().to_string()); return Err(result.err().unwrap().to_string());
@ -191,20 +192,20 @@ pub fn fetch_game(id: DownloadableMetadata, app: tauri::AppHandle) -> Result<Fet
} }
#[tauri::command] #[tauri::command]
pub fn fetch_game_status(meta: DownloadableMetadata) -> Result<ApplicationStatusWithTransient, String> { pub fn fetch_game_status(id: String) -> Result<GameStatusWithTransient, String> {
let status = DownloadStatusManager::fetch_state(&meta); let status = GameStatusManager::fetch_state(&id);
Ok(status) Ok(status)
} }
fn fetch_game_verion_options_logic<'a>( fn fetch_game_verion_options_logic<'a>(
meta: DownloadableMetadata, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, RemoteAccessError> { ) -> Result<Vec<GameVersionOption>, RemoteAccessError> {
let base_url = DB.fetch_base_url(); let base_url = DB.fetch_base_url();
let endpoint = let endpoint =
base_url.join(format!("/api/v1/client/metadata/versions?id={}", meta.id).as_str())?; base_url.join(format!("/api/v1/client/metadata/versions?id={}", game_id).as_str())?;
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
@ -233,51 +234,49 @@ fn fetch_game_verion_options_logic<'a>(
Ok(data) Ok(data)
} }
#[tauri::command]
pub fn fetch_game_verion_options<'a>(
game_id: DownloadableMetadata,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, String> {
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
pub fn uninstall_game( pub fn uninstall_game(
game_id: DownloadableMetadata, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> { ) -> Result<(), String> {
let state_lock = state.lock().unwrap(); let state_lock = state.lock().unwrap();
state_lock.download_manager.uninstall_application(game_id); let meta = get_current_meta(&game_id)?;
state_lock.download_manager.uninstall_application(meta);
drop(state_lock); drop(state_lock);
Ok(()) Ok(())
} }
pub fn push_game_update(app_handle: &AppHandle, meta: DownloadableMetadata, status: ApplicationStatusWithTransient) { pub fn get_current_meta(game_id: &String) -> Result<DownloadableMetadata, String> {
app_handle match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) {
.emit( Some(meta) => Ok(meta.clone()),
&format!("update_game/{}", meta.id), None => Err(String::from("Could not find installed version")),
GameUpdateEvent { }
game_id: meta.id, }
status,
}, #[tauri::command]
) pub fn fetch_game_verion_options<'a>(
.unwrap(); game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, String> {
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
} }
pub fn on_game_complete( pub fn on_game_complete(
meta: DownloadableMetadata, meta: &DownloadableMetadata,
install_dir: String, install_dir: String,
app_handle: &AppHandle, app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> { ) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote // Fetch game version information from remote
let base_url = DB.fetch_base_url(); let base_url = DB.fetch_base_url();
if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound) }
let endpoint = base_url.join( let endpoint = base_url.join(
format!( format!(
"/api/v1/client/metadata/version?id={}&version={}", "/api/v1/client/metadata/version?id={}&version={}",
meta.id, meta.id,
encode(&meta.version) encode(meta.version.as_ref().unwrap())
) )
.as_str(), .as_str(),
)?; )?;
@ -289,34 +288,31 @@ pub fn on_game_complete(
.header("Authorization", header) .header("Authorization", header)
.send()?; .send()?;
if response.status() != 200 { let data = response.json::<GameVersion>()?;
return Err(RemoteAccessError::InvalidResponse(
response.json::<DropServerError>().map_err(|e| {
RemoteAccessError::Generic(format!("failed to parse server error: {}", e))
})?,
));
}
let data = response.json::<ApplicationVersion>()?;
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = DB.borrow_data_mut().unwrap();
handle handle
.applications .applications
.versions .game_versions
.entry(meta.clone()) .entry(meta.id.clone())
.or_default() .or_default()
.insert(meta.version.clone(), data.clone()); .insert(meta.version.clone().unwrap(), data.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
drop(handle); drop(handle);
DB.save().unwrap(); DB.save().unwrap();
let status = if data.setup_command.is_empty() { let status = if data.setup_command.is_empty() {
ApplicationStatus::Installed { GameDownloadStatus::Installed {
version_name: (*meta.version.clone()).to_string(), version_name: meta.version.clone().unwrap(),
install_dir, install_dir,
} }
} else { } else {
ApplicationStatus::SetupRequired { GameDownloadStatus::SetupRequired {
version_name: (*meta.version.clone()).to_string(), version_name: meta.version.clone().unwrap(),
install_dir, install_dir,
} }
}; };
@ -324,15 +320,15 @@ pub fn on_game_complete(
let mut db_handle = DB.borrow_data_mut().unwrap(); let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle db_handle
.applications .applications
.statuses .game_statuses
.insert(meta.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
DB.save().unwrap(); DB.save().unwrap();
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
GameUpdateEvent { GameUpdateEvent {
game_id: (*meta.id.clone()).to_string(), game_id: meta.id.clone(),
status: (Some(status), None), status: (Some(status), None),
}, },
) )
@ -340,3 +336,15 @@ pub fn on_game_complete(
Ok(()) Ok(())
} }
pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) {
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status,
},
)
.unwrap();
}

View File

@ -1,16 +1,26 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState}; use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, AppState, DB};
#[tauri::command] #[tauri::command]
pub fn launch_game( pub fn launch_game(
game_id: DownloadableMetadata, id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> { ) -> Result<(), String> {
let state_lock = state.lock().unwrap(); let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock.launch_process(game_id)?; let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() {
Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name,
Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")),
_ => return Err(String::from("Game not installed"))
};
let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game };
process_manager_lock.launch_process(meta)?;
drop(process_manager_lock); drop(process_manager_lock);
drop(state_lock); drop(state_lock);

View File

@ -15,7 +15,7 @@ use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder; use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{ use crate::{
db::{ApplicationStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, library::push_game_update, state::DownloadStatusManager, AppState, DB db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, library::push_game_update, state::GameStatusManager, AppState, DB
}; };
pub struct ProcessManager<'a> { pub struct ProcessManager<'a> {
@ -106,18 +106,18 @@ impl ProcessManager<'_> {
let mut db_handle = DB.borrow_data_mut().unwrap(); let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle.applications.transient_statuses.remove(&meta); db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.statuses.get(&meta).cloned(); let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
if let Some(saved_state) = current_state { if let Some(saved_state) = current_state {
if let ApplicationStatus::SetupRequired { if let GameDownloadStatus::SetupRequired {
version_name, version_name,
install_dir, install_dir,
} = saved_state } = saved_state
{ {
if let Ok(exit_code) = result { if let Ok(exit_code) = result {
if exit_code.success() { if exit_code.success() {
db_handle.applications.statuses.insert( db_handle.applications.game_statuses.insert(
meta.clone(), meta.id.clone(),
ApplicationStatus::Installed { GameDownloadStatus::Installed {
version_name: version_name.to_string(), version_name: version_name.to_string(),
install_dir: install_dir.to_string(), install_dir: install_dir.to_string(),
}, },
@ -128,9 +128,9 @@ impl ProcessManager<'_> {
} }
drop(db_handle); drop(db_handle);
let status = DownloadStatusManager::fetch_state(&meta); let status = GameStatusManager::fetch_state(&meta.id);
push_game_update(&self.app_handle, meta.clone(), status); push_game_update(&self.app_handle, &meta, status);
// TODO better management // TODO better management
} }
@ -148,18 +148,20 @@ impl ProcessManager<'_> {
} }
let mut db_lock = DB.borrow_data_mut().unwrap(); let mut db_lock = DB.borrow_data_mut().unwrap();
info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions);
let game_status = db_lock let game_status = db_lock
.applications .applications
.statuses .game_statuses
.get(&meta) .get(&meta.id)
.ok_or("Game not installed")?; .ok_or("Game not installed")?;
let status_metadata: Option<(&String, &String)> = match game_status { let status_metadata: Option<(&String, &String)> = match game_status {
ApplicationStatus::Installed { GameDownloadStatus::Installed {
version_name, version_name,
install_dir, install_dir,
} => Some((version_name, install_dir)), } => Some((version_name, install_dir)),
ApplicationStatus::SetupRequired { GameDownloadStatus::SetupRequired {
version_name, version_name,
install_dir, install_dir,
} => Some((version_name, install_dir)), } => Some((version_name, install_dir)),
@ -174,18 +176,18 @@ impl ProcessManager<'_> {
let game_version = db_lock let game_version = db_lock
.applications .applications
.versions .game_versions
.get(&meta) .get(&meta.id)
.ok_or("Invalid game ID".to_owned())? .ok_or("Invalid game ID".to_owned())?
.get(version_name) .get(version_name)
.ok_or("Invalid version name".to_owned())?; .ok_or("Invalid version name".to_owned())?;
let raw_command: String = match game_status { let raw_command: String = match game_status {
ApplicationStatus::Installed { GameDownloadStatus::Installed {
version_name: _, version_name: _,
install_dir: _, install_dir: _,
} => game_version.launch_command.clone(), } => game_version.launch_command.clone(),
ApplicationStatus::SetupRequired { GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir: _, install_dir: _,
} => game_version.setup_command.clone(), } => game_version.setup_command.clone(),
@ -210,7 +212,7 @@ impl ProcessManager<'_> {
.create(true) .create(true)
.open( .open(
self.log_output_dir self.log_output_dir
.join(format!("{}-{}-{}.log", meta.id, meta.version, current_time.timestamp())), .join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())),
) )
.map_err(|v| v.to_string())?; .map_err(|v| v.to_string())?;
@ -221,8 +223,8 @@ impl ProcessManager<'_> {
.create(true) .create(true)
.open(self.log_output_dir.join(format!( .open(self.log_output_dir.join(format!(
"{}-{}-{}-error.log", "{}-{}-{}-error.log",
meta.id, meta.id.clone(),
meta.version, meta.version.clone().unwrap_or_default(),
current_time.timestamp() current_time.timestamp()
))) )))
.map_err(|v| v.to_string())?; .map_err(|v| v.to_string())?;
@ -255,7 +257,7 @@ impl ProcessManager<'_> {
push_game_update( push_game_update(
&self.app_handle, &self.app_handle,
meta.clone(), &meta,
(None, Some(ApplicationTransientStatus::Running {})), (None, Some(ApplicationTransientStatus::Running {})),
); );

View File

@ -1,24 +1,23 @@
use std::sync::Arc;
use crate::{ use crate::{
db::{ApplicationStatus, ApplicationTransientStatus}, download_manager::downloadable_metadata::DownloadableMetadata, DB db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB
}; };
pub type GameStatusWithTransient = (Option<ApplicationStatus>, Option<ApplicationTransientStatus>); pub type GameStatusWithTransient = (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>);
pub struct GameStatusManager {} pub struct GameStatusManager {}
impl DownloadStatusManager { impl GameStatusManager {
pub fn fetch_state(id: &DownloadableMetadata) -> ApplicationStatusWithTransient { pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
let db_lock = DB.borrow_data().unwrap(); let db_lock = DB.borrow_data().unwrap();
GameStatusManager::fetch_state_with_db(game_id, &db_lock) let online_state = match db_lock.applications.installed_game_version.get(game_id) {
} Some(meta) => db_lock
pub fn fetch_state_with_db( .applications
game_id: &String, .transient_statuses
db_lock: &Database, .get(meta)
) -> GameStatusWithTransient { .cloned(),
let offline_state = db_lock.applications.statuses.get(game_id).cloned(); None => None,
let online_state = db_lock.applications.transient_statuses.get(game_id).cloned(); };
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
drop(db_lock);
if online_state.is_some() { if online_state.is_some() {
return (None, online_state); return (None, online_state);

View File

@ -1,5 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use tauri::AppHandle;
use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject}; use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject};
pub struct ToolDownloadAgent { pub struct ToolDownloadAgent {
@ -10,7 +12,7 @@ pub struct ToolDownloadAgent {
progress: Arc<ProgressObject>, progress: Arc<ProgressObject>,
} }
impl Downloadable for ToolDownloadAgent { impl Downloadable for ToolDownloadAgent {
fn download(&self) -> Result<bool, ApplicationDownloadError> { fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
todo!() todo!()
} }

View File

@ -59,3 +59,16 @@ export type GameStatus = {
type: GameStatusEnum; type: GameStatusEnum;
version_name?: string; version_name?: string;
}; };
export enum DownloadableType {
Game = "Game",
Tool = "Tool",
DLC = "DLC",
Mod = "Mod"
}
export type DownloadableMetadata = {
id: string,
version: string,
downloadType: DownloadableType
}

View File

@ -0,0 +1,9 @@
import { type DownloadableMetadata, DownloadableType } from '~/types'
export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
return {
id: gameId,
version,
downloadType: DownloadableType.Game
}
}