Depot API & executor launch (#173)

* feat: depot api downloads

* feat: frontend fixes and experimental webview store

* feat: sync downloader

* feat: cleanup and fixes

* feat: encrypted database and fixed resuming

* feat: launch option selector

* fix: autostart when no options

* fix: clippy

* fix: clippy x2

* feat: executor launch

* feat: executor launch

* feat: not installed error handling

* feat: better offline handling

* feat: dependency popup

* fix: cancelation and resuming issues

* feat: dedup by platform

* feat: new ui for additional components and fix dl manager clog

* feat: auto-queue dependencies

* feat: depot scanning and ranking

* feat: new library fetching stack

* In-app store page (Windows + macOS) (#176)

* feat: async store loading

* feat: fix overscroll behaviour

* fix: query params in server protocol

* fix: clippy
This commit is contained in:
DecDuck
2026-01-20 00:40:48 +00:00
committed by GitHub
parent 55fdaf51e1
commit fc69ae30ab
72 changed files with 3430 additions and 2732 deletions
+15 -12
View File
@@ -3,6 +3,7 @@ use std::sync::nonpoison::Mutex;
use database::{borrow_db_checked, borrow_db_mut_checked};
use download_manager::DOWNLOAD_MANAGER;
use log::{debug, error};
use remote::requests::{generate_url, make_authenticated_get};
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
use tauri_plugin_opener::OpenerExt;
@@ -18,18 +19,15 @@ pub fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<String, S
}
#[tauri::command]
pub fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app);
pub async fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app).await;
}
pub fn cleanup_and_exit(app: &AppHandle) {
pub async fn cleanup_and_exit(app: &AppHandle) {
debug!("cleaning up and exiting application");
match DOWNLOAD_MANAGER.ensure_terminated() {
Ok(res) => match res {
Ok(()) => debug!("download manager terminated correctly"),
Err(()) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{e:?}"),
match DOWNLOAD_MANAGER.ensure_terminated().await {
Ok(()) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
}
app.exit(0);
@@ -76,7 +74,12 @@ pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autost
#[tauri::command]
pub fn open_fs(path: String, app_handle: AppHandle) -> Result<(), tauri_plugin_opener::Error> {
app_handle
.opener()
.open_path(path, None::<&str>)
app_handle.opener().open_path(path, None::<&str>)
}
#[tauri::command]
pub async fn check_online() -> Result<bool, ()> {
let online = make_authenticated_get(generate_url(&["/api/v1/"], &[]).unwrap()).await.is_ok();
Ok(online)
}
+38 -78
View File
@@ -1,15 +1,29 @@
use games::collections::collection::{Collection, Collections};
use std::sync::nonpoison::Mutex;
use client::app_state::AppState;
use database::{GameDownloadStatus, borrow_db_checked};
use games::collections::collection::Collections;
use remote::{
auth::generate_authorization_header,
cache::{cache_object, get_cached_object},
error::RemoteAccessError,
offline,
requests::{generate_url, make_authenticated_get},
utils::DROP_CLIENT_ASYNC,
};
use serde_json::json;
#[tauri::command]
pub async fn fetch_collections(
state: tauri::State<'_, Mutex<AppState>>,
hard_refresh: Option<bool>,
) -> Result<Collections, RemoteAccessError> {
offline!(
state,
fetch_collections_online,
fetch_collections_offline,
hard_refresh
)
.await
}
pub async fn fetch_collections_online(
hard_refresh: Option<bool>,
) -> Result<Collections, RemoteAccessError> {
let do_hard_refresh = hard_refresh.unwrap_or(false);
@@ -28,79 +42,25 @@ pub async fn fetch_collections(
Ok(collections)
}
#[tauri::command]
pub async fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
let response = make_authenticated_get(generate_url(
&["/api/v1/client/collection/", &collection_id],
&[],
)?)
.await?;
pub async fn fetch_collections_offline(
_hard_refresh: Option<bool>,
) -> Result<Collections, RemoteAccessError> {
let mut cached = get_cached_object::<Collections>("collections")?;
Ok(response.json().await?)
}
let db_handle = borrow_db_checked();
#[tauri::command]
pub async fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let url = generate_url(&["/api/v1/client/collection"], &[])?;
for collection in cached.iter_mut() {
collection.entries.retain(|v| {
matches!(
&db_handle
.applications
.game_statuses
.get(&v.game_id)
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
});
}
let response = client
.post(url)
.header("Authorization", generate_authorization_header())
.json(&json!({"name": name}))
.send()
.await?;
Ok(response.json().await?)
}
#[tauri::command]
pub async fn add_game_to_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
client
.post(url)
.header("Authorization", generate_authorization_header())
.json(&json!({"id": game_id}))
.send()
.await?;
Ok(())
}
#[tauri::command]
pub async fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let url = generate_url(&["/api/v1/client/collection", &collection_id], &[])?;
let response = client
.delete(url)
.header("Authorization", generate_authorization_header())
.send()
.await?;
Ok(response.json().await?)
}
#[tauri::command]
pub async fn delete_game_in_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
client
.delete(url)
.header("Authorization", generate_authorization_header())
.json(&json!({"id": game_id}))
.send()
.await?;
Ok(())
}
Ok(cached)
}
+8 -8
View File
@@ -2,21 +2,21 @@ use database::DownloadableMetadata;
use download_manager::DOWNLOAD_MANAGER;
#[tauri::command]
pub fn pause_downloads() {
DOWNLOAD_MANAGER.pause_downloads();
pub async fn pause_downloads() {
DOWNLOAD_MANAGER.pause_downloads().await;
}
#[tauri::command]
pub fn resume_downloads() {
DOWNLOAD_MANAGER.resume_downloads();
pub async fn resume_downloads() {
DOWNLOAD_MANAGER.resume_downloads().await;
}
#[tauri::command]
pub fn move_download_in_queue(old_index: usize, new_index: usize) {
DOWNLOAD_MANAGER.rearrange(old_index, new_index);
pub async fn move_download_in_queue(old_index: usize, new_index: usize) {
DOWNLOAD_MANAGER.rearrange(old_index, new_index).await;
}
#[tauri::command]
pub fn cancel_game(meta: DownloadableMetadata) {
DOWNLOAD_MANAGER.cancel(meta);
pub async fn cancel_game(meta: DownloadableMetadata) {
DOWNLOAD_MANAGER.cancel(meta).await;
}
+61 -27
View File
@@ -1,6 +1,8 @@
use std::{path::PathBuf, sync::Arc};
use database::{GameDownloadStatus, borrow_db_checked};
use database::{
DownloadType, DownloadableMetadata, GameDownloadStatus, borrow_db_checked, platform::Platform,
};
use download_manager::{
DOWNLOAD_MANAGER, downloadable::Downloadable, error::ApplicationDownloadError,
};
@@ -9,16 +11,37 @@ use games::downloads::download_agent::GameDownloadAgent;
#[tauri::command]
pub async fn download_game(
game_id: String,
game_version: String,
version_id: String,
target_platform: Platform,
install_dir: usize,
) -> Result<(), ApplicationDownloadError> {
{
let db = borrow_db_checked();
let status = db
.applications
.game_statuses
.get(&game_id)
.unwrap_or(&GameDownloadStatus::Remote {});
if matches!(status, GameDownloadStatus::Installed { .. }) {
return Ok(());
}
};
let sender = { DOWNLOAD_MANAGER.get_sender().clone() };
let meta = DownloadableMetadata {
id: game_id,
version: version_id,
target_platform,
download_type: DownloadType::Game,
};
let game_download_agent = GameDownloadAgent::new_from_index(
game_id.clone(),
game_version.clone(),
meta,
install_dir,
sender,
DOWNLOAD_MANAGER.clone_depot_manager(),
)
.await?;
@@ -27,6 +50,7 @@ pub async fn download_game(
DOWNLOAD_MANAGER
.queue_download(game_download_agent.clone())
.await
.unwrap();
Ok(())
@@ -34,43 +58,53 @@ pub async fn download_game(
#[tauri::command]
pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadError> {
let s = borrow_db_checked()
.applications
.game_statuses
.get(&game_id)
.unwrap()
.clone();
let (meta, install_dir) = {
let db_lock = borrow_db_checked();
let status = db_lock
.applications
.game_statuses
.get(&game_id)
.ok_or(ApplicationDownloadError::InvalidCommand)?
.clone();
let (version_name, install_dir) = match s {
GameDownloadStatus::Remote {} => unreachable!(),
GameDownloadStatus::SetupRequired { .. } => unreachable!(),
GameDownloadStatus::Installed { .. } => unreachable!(),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
let meta = db_lock
.applications
.installed_game_version
.get(&game_id)
.ok_or(ApplicationDownloadError::InvalidCommand)?
.clone();
let install_dir = match status {
GameDownloadStatus::Remote {} => Err(ApplicationDownloadError::InvalidCommand),
GameDownloadStatus::SetupRequired { .. } => {
Err(ApplicationDownloadError::InvalidCommand)
}
GameDownloadStatus::Installed { .. } => Err(ApplicationDownloadError::InvalidCommand),
GameDownloadStatus::PartiallyInstalled { install_dir, .. } => Ok(install_dir),
}?;
(meta, install_dir)
};
let sender = DOWNLOAD_MANAGER.get_sender();
let parent_dir: PathBuf = install_dir.into();
let install_dir = PathBuf::from(install_dir);
let install_dir = install_dir
.parent()
.expect("game somehow installed at root");
let game_download_agent = Arc::new(Box::new(
GameDownloadAgent::new(
game_id,
version_name.clone(),
parent_dir
.parent()
.unwrap_or_else(|| {
panic!("Failed to get parent directry of {}", parent_dir.display())
})
.to_path_buf(),
meta,
install_dir.to_path_buf(),
sender,
DOWNLOAD_MANAGER.clone_depot_manager(),
)
.await?,
) as Box<dyn Downloadable + Send + Sync>);
DOWNLOAD_MANAGER
.queue_download(game_download_agent)
.await
.unwrap();
Ok(())
}
+148 -88
View File
@@ -1,44 +1,60 @@
use std::sync::nonpoison::Mutex;
use database::{GameDownloadStatus, GameVersion, borrow_db_checked, borrow_db_mut_checked};
use bitcode::{Decode, Encode};
use database::{
DownloadableMetadata, GameDownloadStatus, borrow_db_checked,
borrow_db_mut_checked, platform::Platform,
};
use games::{
collections::collection::Collection,
downloads::error::LibraryError,
library::{FetchGameStruct, FrontendGameOptions, Game, get_current_meta, uninstall_game_logic},
state::{GameStatusManager, GameStatusWithTransient},
};
use log::{info, warn};
use log::warn;
use process::PROCESS_MANAGER;
use remote::{
auth::generate_authorization_header,
cache::{cache_object, cache_object_db, get_cached_object, get_cached_object_db},
cache::{cache_object, cache_object_db, get_cached_object},
error::{DropServerError, RemoteAccessError},
offline,
requests::generate_url,
utils::DROP_CLIENT_ASYNC,
};
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use crate::AppState;
use crate::{AppState, collections::fetch_collections};
#[tauri::command]
pub async fn fetch_library(
state: tauri::State<'_, Mutex<AppState>>,
app_handle: AppHandle,
hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
) -> Result<FetchLibraryResponse, RemoteAccessError> {
offline!(
state,
fetch_library_logic,
fetch_library_logic_offline,
state,
app_handle,
hard_refresh
)
.await
}
#[derive(Encode, Decode, Serialize)]
pub struct FetchLibraryResponse {
library: Vec<Game>,
collections: Vec<Collection>,
other: Vec<Game>,
}
pub async fn fetch_library_logic(
state: tauri::State<'_, Mutex<AppState>>,
app_handle: AppHandle,
hard_fresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
) -> Result<FetchLibraryResponse, RemoteAccessError> {
let do_hard_refresh = hard_fresh.unwrap_or(false);
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
return Ok(library);
@@ -62,57 +78,83 @@ pub async fn fetch_library_logic(
return Err(RemoteAccessError::InvalidResponse(err));
}
let mut games: Vec<Game> = response.json().await?;
let library: Vec<Game> = response.json().await?;
let collections = fetch_collections(state, hard_fresh).await?;
let mut handle = state.lock();
let mut all_games = library.clone();
all_games.extend(
collections
.iter()
.flat_map(|v| v.entries.iter().map(|v| v.game.clone())),
);
let mut db_handle = borrow_db_mut_checked();
let installed_metas = {
let mut db_handle = borrow_db_mut_checked();
for game in &games {
handle.games.insert(game.id().clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(game.id()) {
db_handle
.applications
.game_statuses
.insert(game.id().clone(), GameDownloadStatus::Remote {});
for game in &all_games {
if !db_handle.applications.game_statuses.contains_key(game.id()) {
db_handle
.applications
.game_statuses
.insert(game.id().clone(), GameDownloadStatus::Remote {});
}
cache_object_db(&format!("game/{}", game.id), game, &db_handle)?;
}
}
db_handle
.applications
.installed_game_version
.values()
.cloned()
.collect::<Vec<DownloadableMetadata>>()
};
// Add games that are installed but no longer in library
for meta in db_handle.applications.installed_game_version.values() {
if games.iter().any(|e| *e.id() == meta.id) {
let mut other = Vec::new();
for meta in installed_metas {
if all_games.iter().any(|e| *e.id() == meta.id) {
continue;
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = match get_cached_object_db::<Game>(&meta.id.clone(), &db_handle) {
let game = match get_cached_object::<Game>(&meta.id.clone()) {
Ok(game) => game,
Err(err) => {
warn!(
"{} is installed, but encountered error fetching its error: {}.",
meta.id, err
);
/*
* We can't return a dummy object here because it needs to be in the cache to work
* So we uninstall the game so we don't "lose" it
*/
uninstall_game_logic(meta.clone(), &app_handle);
continue;
}
};
games.push(game);
other.push(game);
}
drop(handle);
drop(db_handle);
cache_object("library", &games)?;
let response = FetchLibraryResponse {
library,
collections,
other,
};
Ok(games)
cache_object("library", &response)?;
Ok(response)
}
pub async fn fetch_library_logic_offline(
_state: tauri::State<'_, Mutex<AppState>>,
_app_handle: AppHandle,
_hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let mut games: Vec<Game> = get_cached_object("library")?;
) -> Result<FetchLibraryResponse, RemoteAccessError> {
let mut response: FetchLibraryResponse = get_cached_object("library")?;
let db_handle = borrow_db_checked();
games.retain(|game| {
let retain_filter = |game: &Game| {
matches!(
&db_handle
.applications
@@ -121,70 +163,74 @@ pub async fn fetch_library_logic_offline(
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
};
response.library.retain(retain_filter);
response.other.retain(retain_filter);
response.collections.iter_mut().for_each(|k| {
k.entries.retain(|object| {
matches!(
&db_handle
.applications
.game_statuses
.get(object.game.id())
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
})
});
Ok(games)
Ok(response)
}
pub async fn fetch_game_logic(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let version = {
let state_handle = state.lock();
let db_lock = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
let version = match metadata_option {
match metadata_option {
None => None,
Some(metadata) => db_lock
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.get(&metadata.version)
.cloned(),
};
let game = state_handle.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id, &db_lock);
let data = FetchGameStruct::new(game.clone(), status, version);
cache_object_db(&id, game, &db_lock)?;
return Ok(data);
}
version
};
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.send()
.await?;
let game = match get_cached_object::<Game>(&format!("game/{}", id)) {
Ok(value) => value,
Err(_) => {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game", &id], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.send()
.await?;
if response.status() == 404 {
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
if let Ok(fetch_data) = offline_fetch {
return Ok(fetch_data);
if response.status() == 404 {
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
if let Ok(fetch_data) = offline_fetch {
return Ok(fetch_data);
}
return Err(RemoteAccessError::GameNotFound(id));
}
if response.status() != 200 {
let err = response.json().await?;
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let game: Game = response.json().await?;
game
}
return Err(RemoteAccessError::GameNotFound(id));
}
if response.status() != 200 {
let err = response.json().await?;
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let game: Game = response.json().await?;
let mut state_handle = state.lock();
state_handle.games.insert(id.clone(), game.clone());
};
let mut db_handle = borrow_db_mut_checked();
@@ -205,10 +251,31 @@ pub async fn fetch_game_logic(
Ok(data)
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionDownloadOptionRequiredContent {
version_id: String,
name: String,
icon_object_id: String,
short_description: String,
size: usize,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionDownloadOption {
version_id: String,
display_name: Option<String>,
version_path: String,
platform: Platform,
size: usize,
required_content: Vec<VersionDownloadOptionRequiredContent>,
}
pub async fn fetch_game_version_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
) -> Result<Vec<VersionDownloadOption>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
@@ -223,15 +290,16 @@ pub async fn fetch_game_version_options_logic(
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let data: Vec<GameVersion> = response.json().await?;
let data: Vec<VersionDownloadOption> = response.json().await?;
let state_lock = state.lock();
let process_manager_lock = PROCESS_MANAGER.lock();
let data: Vec<GameVersion> = data
let data = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform))
.collect();
//data.dedup_by_key(|v| v.platform);
drop(process_manager_lock);
drop(state_lock);
@@ -249,8 +317,7 @@ pub async fn fetch_game_logic_offline(
Some(metadata) => db_handle
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.get(&metadata.version)
.cloned(),
};
@@ -298,7 +365,7 @@ pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), Libr
pub async fn fetch_game_version_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
) -> Result<Vec<VersionDownloadOption>, RemoteAccessError> {
fetch_game_version_options_logic(game_id, state).await
}
@@ -314,31 +381,24 @@ pub fn update_game_configuration(
.get(&game_id)
.ok_or(LibraryError::MetaNotFound(game_id))?;
let id = installed_version.id.clone();
let version = installed_version
.version
.clone()
.ok_or(LibraryError::VersionNotFound(id.clone()))?;
let _id = installed_version.id.clone();
let version = installed_version.version.clone();
let mut existing_configuration = handle
.applications
.game_versions
.get(&id)
.unwrap()
.get(&version)
.unwrap()
.clone();
// Add more options in here
existing_configuration.launch_command_template = options.launch_string().clone();
existing_configuration.launch_template = options.launch_string().clone();
// Add no more options past here
handle
.applications
.game_versions
.get_mut(&id)
.unwrap()
.insert(version.to_string(), existing_configuration);
Ok(())
+43 -61
View File
@@ -8,26 +8,24 @@
#![deny(clippy::all)]
use std::{
collections::HashMap, env, fs::File, io::Write, panic::PanicHookInfo, path::Path, str::FromStr,
env, fs::File, io::Write, panic::PanicHookInfo, path::Path, str::FromStr,
sync::nonpoison::Mutex, time::SystemTime,
};
use ::client::{app_status::AppStatus, autostart::sync_autostart_on_startup, user::User};
use ::client::{app_state::AppState, app_status::AppStatus, autostart::sync_autostart_on_startup};
use ::download_manager::DownloadManagerWrapper;
use ::games::{library::Game, scan::scan_install_dirs};
use ::games::scan::scan_install_dirs;
use ::process::ProcessManagerWrapper;
use ::remote::{
auth::{self, HandshakeRequestBody, HandshakeResponse, generate_authorization_header},
cache::clear_cached_object,
error::RemoteAccessError,
fetch_object::fetch_object_wrapper,
offline,
server_proto::{handle_server_proto_offline_wrapper, handle_server_proto_wrapper},
utils::DROP_CLIENT_ASYNC,
server_proto::handle_server_proto_wrapper,
utils::{DROP_APP_HANDLE, DROP_CLIENT_ASYNC},
};
use database::{
DB, GameDownloadStatus, borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR,
interface::DatabaseImpls,
};
use log::{LevelFilter, debug, info, warn};
use log4rs::{
@@ -36,9 +34,9 @@ use log4rs::{
config::{Appender, Root},
encode::pattern::PatternEncoder,
};
use serde::Serialize;
use tauri::{
AppHandle, Manager, RunEvent, WindowEvent,
AppHandle, LogicalPosition, LogicalSize, Manager, RunEvent, WebviewBuilder, WebviewUrl,
WindowBuilder, WindowEvent,
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::TrayIconBuilder,
};
@@ -47,8 +45,6 @@ use tauri_plugin_dialog::DialogExt;
use url::Url;
use utils::app_emit;
use crate::client::cleanup_and_exit;
mod client;
mod collections;
mod download_manager;
@@ -59,7 +55,6 @@ mod remote;
mod settings;
use client::*;
use collections::*;
use download_manager::*;
use downloads::*;
use games::*;
@@ -67,14 +62,6 @@ use process::*;
use remote::*;
use settings::*;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState {
status: AppStatus,
user: Option<User>,
games: HashMap<String, Game>,
}
async fn setup(handle: AppHandle) -> AppState {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
@@ -106,8 +93,6 @@ async fn setup(handle: AppHandle) -> AppState {
log4rs::init_config(config).expect("Failed to initialise log4rs");
let games = HashMap::new();
ProcessManagerWrapper::init(handle.clone());
DownloadManagerWrapper::init(handle.clone());
@@ -120,7 +105,6 @@ async fn setup(handle: AppHandle) -> AppState {
return AppState {
status: AppStatus::NotConfigured,
user: None,
games,
};
}
@@ -182,7 +166,6 @@ async fn setup(handle: AppHandle) -> AppState {
AppState {
status: app_status,
user,
games,
}
}
@@ -204,6 +187,8 @@ pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// let global_span = span!(Level::TRACE, "global_span");
// let _enter = global_span.enter();
std::panic::set_hook(Box::new(|e| {
let _ = custom_panic_handler(e);
println!("{e}");
@@ -243,6 +228,7 @@ pub fn run() {
use_remote,
gen_drop_url,
fetch_drop_object,
check_online,
// Library
fetch_library,
fetch_game,
@@ -252,13 +238,6 @@ pub fn run() {
fetch_game_status,
fetch_game_version_options,
update_game_configuration,
// Collections
fetch_collections,
fetch_collection,
create_collection,
add_game_to_collection,
delete_collection,
delete_game_in_collection,
// Downloads
download_game,
resume_download,
@@ -272,7 +251,8 @@ pub fn run() {
kill_game,
toggle_autostart,
get_autostart_enabled,
open_process_logs
open_process_logs,
get_launch_options
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
@@ -284,10 +264,16 @@ pub fn run() {
let handle = app.handle().clone();
tauri::async_runtime::block_on(async move {
let state = setup(handle).await;
let state = setup(handle.clone()).await;
info!("initialized drop client");
app.manage(Mutex::new(state));
let global_app_handle = handle;
{
let mut app_handle_lock = DROP_APP_HANDLE.lock().await;
app_handle_lock.replace(global_app_handle);
};
{
use tauri_plugin_deep_link::DeepLinkExt;
let _ = app.deep_link().register_all();
@@ -296,19 +282,26 @@ pub fn run() {
let handle = app.handle().clone();
let _main_window = tauri::WebviewWindowBuilder::new(
&handle,
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
tauri::WebviewUrl::App("main".into()),
)
.title("Drop Desktop App")
.min_inner_size(1000.0, 500.0)
.inner_size(1536.0, 864.0)
.decorations(false)
.shadow(false)
.data_directory(DATA_ROOT_DIR.join(".webview"))
.build()
.expect("Failed to build main window");
let width = 1536.0;
let height = 864.0;
let main_window = WindowBuilder::new(&handle, "main")
.title("Drop Desktop App")
.min_inner_size(1000.0, 500.0)
.inner_size(width, height)
.decorations(false)
.shadow(false)
.build()
.expect("failed to build main window");
main_window
.add_child(
WebviewBuilder::new("frontned", WebviewUrl::App("main".into()))
.auto_resize(),
LogicalPosition::new(0., 0.),
LogicalSize::new(width, height),
)
.expect("failed to create frontend webview");
app.deep_link().on_open_url(move |event| {
debug!("handling drop:// url");
@@ -368,7 +361,7 @@ pub fn run() {
.expect("Failed to show window");
}
"quit" => {
cleanup_and_exit(app);
app.exit(0);
}
_ => {
@@ -408,20 +401,9 @@ pub fn run() {
fetch_object_wrapper(request, responder).await;
});
})
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
tauri::async_runtime::block_on(async move {
let state = ctx
.app_handle()
.state::<tauri::State<'_, Mutex<AppState>>>();
offline!(
state,
handle_server_proto_wrapper,
handle_server_proto_offline_wrapper,
request,
responder
)
.await;
.register_asynchronous_uri_scheme_protocol("server", |_ctx, request, responder| {
tauri::async_runtime::spawn(async move {
handle_server_proto_wrapper(request, responder).await;
});
})
.on_window_event(|window, event| {
+37 -25
View File
@@ -1,41 +1,53 @@
use std::sync::nonpoison::Mutex;
use std::sync::Arc;
use process::{PROCESS_MANAGER, error::ProcessError};
use process::{
PROCESS_MANAGER,
error::ProcessError,
process_manager::{LaunchOption, ProcessManager},
};
use serde::Serialize;
use tauri::AppHandle;
use tauri_plugin_opener::OpenerExt;
use crate::AppState;
#[tauri::command]
pub fn get_launch_options(id: String) -> Result<Vec<LaunchOption>, ProcessError> {
let launch_options = ProcessManager::get_launch_options(id)?;
Ok(launch_options)
}
#[derive(Serialize)]
#[serde(tag = "result", content = "data")]
pub enum LaunchResult {
Success,
InstallRequired(String, String),
}
#[tauri::command]
pub fn launch_game(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> {
let state_lock = state.lock();
let mut process_manager_lock = PROCESS_MANAGER.lock();
//let meta = DownloadableMetadata {
// id,
// version: Some(version),
// download_type: DownloadType::Game,
//};
pub fn launch_game(id: String, index: usize) -> Result<LaunchResult, ProcessError> {
let result = {
let mut process_manager_lock = PROCESS_MANAGER.lock();
match process_manager_lock.launch_process(id) {
Ok(()) => {}
Err(e) => return Err(e),
process_manager_lock.launch_process(id, index)
};
if let Err(err) = &result
&& let ProcessError::RequiredDependency(game_id, version_id) = err
{
return Ok(LaunchResult::InstallRequired(
game_id.to_string(),
version_id.to_string(),
));
}
drop(process_manager_lock);
drop(state_lock);
result?;
Ok(())
Ok(LaunchResult::Success)
}
#[tauri::command]
pub fn kill_game(game_id: String) -> Result<(), ProcessError> {
PROCESS_MANAGER
.lock()
.kill_game(game_id)
.map_err(ProcessError::IOError)
Ok(PROCESS_MANAGER.lock().kill_game(game_id)?)
}
#[tauri::command]
@@ -46,5 +58,5 @@ pub fn open_process_logs(game_id: String, app_handle: AppHandle) -> Result<(), P
app_handle
.opener()
.open_path(dir.display().to_string(), None::<&str>)
.map_err(ProcessError::OpenerError)
.map_err(|v| ProcessError::OpenerError(Arc::new(v)))
}