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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user