diff --git a/Cargo.lock b/Cargo.lock index 0e94193..d6d9e58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -121,6 +130,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -172,11 +196,14 @@ version = "1.5.1" dependencies = [ "base64", "chrono", + "color-eyre", "config", "copypasta", "dashmap", + "dyn-clone", "eframe", "egui", + "eyre", "flate2", "futures", "image", @@ -368,6 +395,33 @@ dependencies = [ "objc", ] +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -669,6 +723,12 @@ dependencies = [ "wio", ] +[[package]] +name = "dyn-clone" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" + [[package]] name = "eframe" version = "0.19.0" @@ -829,6 +889,16 @@ dependencies = [ "threadpool", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -1099,6 +1169,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1355,6 +1431,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.8.1" @@ -1790,9 +1872,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" dependencies = [ "winapi", ] @@ -1887,6 +1969,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.13.1" @@ -1972,6 +2063,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.0" @@ -2302,6 +2399,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rusty-leveldb" version = "1.0.4" @@ -2479,6 +2582,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_library" version = "0.1.9" @@ -2670,9 +2782,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.25.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1594a36887d0f70096702bffadfb67dfbfe76ad4bf84605e86157dc9fce9961a" +checksum = "7890fff842b8db56f2033ebee8f6efe1921475c3830c115995552914fb967580" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2717,6 +2829,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -2888,6 +3009,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -2953,6 +3096,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index eeba94f..b2ec37f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,10 @@ serde_json = "^1.0.83" sqlite = "^0.27.0" steam_shortcuts_util = "^1.1.8" steamgriddb_api = "^0.3.1" -sysinfo = "^0.25.2" - +sysinfo = "^0.26.4" +eyre = "^0.6.8" +color-eyre = "^0.6" +dyn-clone = "^1.0.9" [dependencies.dashmap] features = ["serde"] diff --git a/src/amazon/amazon_game.rs b/src/amazon/amazon_game.rs deleted file mode 100644 index b56f6f6..0000000 --- a/src/amazon/amazon_game.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::path::{Path, PathBuf}; - -use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; - -#[derive(Debug, Clone)] -pub struct AmazonGame { - pub title: String, - pub id: String, - pub launcher_path: PathBuf, -} - -impl From for ShortcutOwned { - fn from(game: AmazonGame) -> Self { - let launch = format!("amazon-games://play/{}", game.id); - let exe = game.launcher_path.to_string_lossy().to_string(); - let start_dir = game - .launcher_path - .parent() - .unwrap_or_else(|| Path::new("")) - .to_string_lossy() - .to_string(); - Shortcut::new( - "0", - game.title.as_str(), - exe.as_str(), - start_dir.as_str(), - "", - "", - launch.as_str(), - ) - .to_owned() - } -} diff --git a/src/amazon/amazon_platform.rs b/src/amazon/amazon_platform.rs deleted file mode 100644 index a59bfbf..0000000 --- a/src/amazon/amazon_platform.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{ - error::Error, - path::{Path, PathBuf}, -}; - -use sqlite::State; - -use crate::platform::Platform; - -use super::{AmazonGame, AmazonSettings}; - -pub struct AmazonPlatform { - pub settings: AmazonSettings, -} - -impl Platform> for AmazonPlatform { - #[cfg(windows)] - fn enabled(&self) -> bool { - self.settings.enabled - } - - #[cfg(target_family = "unix")] - fn enabled(&self) -> bool { - false - } - - fn name(&self) -> &str { - "Amazon" - } - - fn get_shortcuts(&self) -> Result, Box> { - let sqllite_path = - get_sqlite_path().expect("This should never get called if settings are invalid"); - let launcher_path = - get_launcher_path().expect("This should never get called if settings are invalid"); - let mut result = vec![]; - let connection = sqlite::open(sqllite_path)?; - let mut statement = - connection.prepare("SELECT Id, ProductTitle FROM DbSet WHERE Installed = 1")?; - while let State::Row = statement.next().unwrap() { - let id = statement.read::(0); - let title = statement.read::(1); - if let (Ok(id), Ok(title)) = (id, title) { - result.push(AmazonGame { - title, - id, - launcher_path: launcher_path.clone(), - }); - } - } - Ok(result) - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let path = get_sqlite_path(); - let launcher = get_launcher_path(); - if path.is_some() && launcher.is_some() { - crate::platform::SettingsValidity::Valid - } else { - crate::platform::SettingsValidity::Invalid { - reason: "Could not find Amazon Games installation".to_string(), - } - } - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn needs_proton(&self, _input: &AmazonGame) -> bool { - false - } -} - -fn get_sqlite_path() -> Option { - match std::env::var("LOCALAPPDATA") { - Ok(localdata) => { - let path = Path::new(&localdata) - .join("Amazon Games") - .join("Data") - .join("Games") - .join("Sql") - .join("GameInstallInfo.sqlite"); - if path.exists() { - Some(path) - } else { - None - } - } - Err(_e) => None, - } -} - -fn get_launcher_path() -> Option { - match std::env::var("LOCALAPPDATA") { - Ok(localdata) => { - let path = Path::new(&localdata) - .join("Amazon Games") - .join("App") - .join("Amazon Games.exe"); - if path.exists() { - Some(path) - } else { - None - } - } - Err(_e) => None, - } -} diff --git a/src/amazon/amazon_settings.rs b/src/amazon/amazon_settings.rs deleted file mode 100644 index 13cd240..0000000 --- a/src/amazon/amazon_settings.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct AmazonSettings { - pub enabled: bool, - pub launcher_location: Option, -} diff --git a/src/amazon/mod.rs b/src/amazon/mod.rs deleted file mode 100644 index afbe9d9..0000000 --- a/src/amazon/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod amazon_platform; -pub use amazon_platform::*; - -pub mod amazon_game; -pub use amazon_game::*; - -pub mod amazon_settings; -pub use amazon_settings::*; diff --git a/src/bottles/settings.rs b/src/bottles/settings.rs deleted file mode 100644 index 7fcb88c..0000000 --- a/src/bottles/settings.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct BottlesSettings { - pub enabled: bool, -} diff --git a/src/config.rs b/src/config.rs index d804450..a7f2ed6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ pub fn get_config_folder() -> PathBuf { let home = std::env::var("HOME"); match (config_home, home) { (Ok(p), _) => Path::new(&p).to_path_buf(), - (Err(_), Ok(home)) => Path::new(&home).join(".config").join("boilr").to_path_buf(), + (Err(_), Ok(home)) => Path::new(&home).join(".config").join("boilr"), _ => Path::new("").to_path_buf(), } } @@ -49,5 +49,5 @@ pub fn get_backups_flder() -> PathBuf { #[cfg(target_family = "unix")] pub fn get_boilr_links_path() -> PathBuf { - get_config_folder().join("links").to_path_buf() + get_config_folder().join("links") } diff --git a/src/defaultconfig.toml b/src/defaultconfig.toml index fd9f725..1bbdfb0 100644 --- a/src/defaultconfig.toml +++ b/src/defaultconfig.toml @@ -8,47 +8,6 @@ prefer_animated = false banned_images = [] only_download_boilr_images = false -[origin] -enabled = true - -[gog] -enabled = true -create_symlinks = true - -[epic_games] -enabled = true -safe_launch = [] - -[uplay] -enabled = true - -[legendary] -enabled = true - -[itch] -enabled = true -create_symlinks = true - -[lutris] -enabled = false -executable = "lutris" -flatpak_image = "net.lutris.Lutris//beta" -flatpak = false - -[heroic] -enabled = true -launch_games_through_heroic =[] -default_launch_through_heroic = true - -[amazon] -enabled = true - -[flatpak] -enabled = true - -[bottles] -enabled = true - [steam] create_collections = false optimize_for_big_picture = false diff --git a/src/egs/epic_platform.rs b/src/egs/epic_platform.rs deleted file mode 100644 index da9e1f1..0000000 --- a/src/egs/epic_platform.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::platform::{Platform, SettingsValidity}; - -use super::{get_egs_manifests, EpicGamesLauncherSettings, ManifestItem}; - -pub struct EpicPlatform { - settings: EpicGamesLauncherSettings, -} - -impl EpicPlatform { - pub fn new(settings: &EpicGamesLauncherSettings) -> Self { - EpicPlatform { - settings: settings.clone(), - } - } -} - -impl Platform for EpicPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "EGS" - } - - fn get_shortcuts(&self) -> Result, String> { - get_egs_manifests(&self.settings) - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn settings_valid(&self) -> SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: format!("{}", err), - }, - } - } - - fn needs_proton(&self, _input: &ManifestItem) -> bool { - #[cfg(target_family = "unix")] - return true; - #[cfg(target_os = "windows")] - return false; - } -} diff --git a/src/flatpak/mod.rs b/src/flatpak/mod.rs deleted file mode 100644 index fc511aa..0000000 --- a/src/flatpak/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod platform; -mod settings; - -pub use platform::*; -pub use settings::*; diff --git a/src/flatpak/settings.rs b/src/flatpak/settings.rs deleted file mode 100644 index fe1dfd5..0000000 --- a/src/flatpak/settings.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct FlatpakSettings { - pub enabled: bool, -} diff --git a/src/gog/gog_settings.rs b/src/gog/gog_settings.rs deleted file mode 100644 index 5ff8ce4..0000000 --- a/src/gog/gog_settings.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GogSettings { - pub enabled: bool, - pub location: Option, - pub wine_c_drive: Option, - #[cfg(target_family = "unix")] - pub create_symlinks: bool, -} diff --git a/src/gog/mod.rs b/src/gog/mod.rs deleted file mode 100644 index b4acfb5..0000000 --- a/src/gog/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod gog_config; -mod gog_game; -mod gog_platform; -mod gog_settings; - -pub use gog_game::GogShortcut; -pub use gog_platform::*; -pub use gog_settings::*; diff --git a/src/heroic/heroic_platform.rs b/src/heroic/heroic_platform.rs deleted file mode 100644 index aa5f0e2..0000000 --- a/src/heroic/heroic_platform.rs +++ /dev/null @@ -1,221 +0,0 @@ -use serde::Deserialize; - -use super::{HeroicGame, HeroicGameType, HeroicSettings}; -use crate::gog::get_shortcuts_from_game_folders; -use crate::platform::{Platform, SettingsValidity}; -use std::collections::HashMap; -use std::error::Error; -use std::path::Path; - -use std::path::PathBuf; - -pub struct HeroicPlatform { - pub settings: HeroicSettings, -} - -#[derive(Deserialize, Debug, Clone, Copy)] -pub enum InstallationMode { - FlatPak, - UserBin, -} - -#[derive(Deserialize)] -struct HeroicGogConfig { - installed: Vec, -} - -#[derive(Deserialize)] -struct HeroicGogPath { - platform: String, - #[serde(alias = "appName")] - app_name: String, - install_path: String, -} - -fn get_installed_json_location(install_mode: &InstallationMode) -> PathBuf { - let home_dir = std::env::var("HOME").unwrap_or_else(|_| "".to_string()); - match install_mode { - InstallationMode::FlatPak => Path::new(&home_dir) - .join(".var/app/com.heroicgameslauncher.hgl/config/legendary/installed.json"), - InstallationMode::UserBin => Path::new(&home_dir).join(".config/legendary/installed.json"), - } -} - -fn get_gog_installed_location(install_mode: &InstallationMode) -> PathBuf { - let home_dir = std::env::var("HOME").unwrap_or_else(|_| "".to_string()); - match install_mode { - InstallationMode::FlatPak => Path::new(&home_dir) - .join(".var/app/com.heroicgameslauncher.hgl/config/heroic/gog_store/installed.json"), - InstallationMode::UserBin => { - Path::new(&home_dir).join(".config/heroic/gog_store/installed.json") - } - } -} - -fn get_shortcuts_from_install_mode( - install_mode: &InstallationMode, -) -> Result, Box> { - let installed_path = get_installed_json_location(install_mode); - get_shortcuts_from_location(installed_path) -} - -fn get_shortcuts_from_location>(path: P) -> Result, Box> { - let installed_json_path = path.as_ref(); - if installed_json_path.exists() { - let json = std::fs::read_to_string(installed_json_path)?; - let games_map = serde_json::from_str::>(&json)?; - let mut games = vec![]; - for game in games_map.values() { - games.push(game.clone()); - } - Ok(games) - } else { - Ok(vec![]) - } -} - -impl HeroicPlatform { - pub fn get_heroic_games(&self) -> Vec { - let install_modes = vec![InstallationMode::FlatPak, InstallationMode::UserBin]; - - let mut heroic_games = self.get_epic_games(&install_modes); - if let Ok(gog_games) = get_gog_games(&self.settings, &install_modes) { - heroic_games.extend(gog_games); - } else { - println!("Did not find any GOG games in heroic") - } - heroic_games - } -} - -impl Platform> for HeroicPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Heroic" - } - - fn get_shortcuts(&self) -> Result, Box> { - Ok(self.get_heroic_games()) - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: format!("{}", err), - }, - } - } - - fn needs_proton(&self, input: &HeroicGameType) -> bool { - match input { - HeroicGameType::Epic(_game) => true, - HeroicGameType::Gog(_, is_windows) => *is_windows, - HeroicGameType::Heroic { .. } => false, - } - } -} - -impl HeroicPlatform { - pub fn get_epic_games(&self, install_modes: &[InstallationMode]) -> Vec { - let mut shortcuts = vec![]; - for install_mode in install_modes { - if let Ok(mut games) = get_shortcuts_from_install_mode(install_mode) { - games.sort_by_key(|m| { - format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name) - }); - games.dedup_by_key(|m| { - format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name) - }); - - for game in games { - if self.settings.is_heroic_launch(&game.app_name) { - shortcuts.push(HeroicGameType::Heroic { - title: game.title, - app_name: game.app_name, - install_mode: *install_mode, - }); - } else { - if game.is_installed() { - shortcuts.push(HeroicGameType::Epic(game)); - } - } - } - } - } - shortcuts - } -} -fn get_gog_games( - settings: &HeroicSettings, - install_modes: &[InstallationMode], -) -> Result, Box> { - let mut gog_paths = vec![]; - for install_mode in install_modes { - let config = get_gog_installed_location(install_mode); - if config.exists() { - if let Ok(config_string) = std::fs::read_to_string(config) { - if let Ok(config) = serde_json::from_str::(&config_string) { - for c in config.installed { - gog_paths.push((install_mode, c)); - } - } - } - } - } - - let mut is_windows_map = HashMap::new(); - - for (_, path) in gog_paths.iter() { - is_windows_map.insert(path.app_name.clone(), path.platform == "windows"); - } - - let mut gog_shortcuts = vec![]; - - let heroic_games = gog_paths - .iter() - .filter(|(_, p)| settings.is_heroic_launch(&p.app_name)) - .filter_map(|(install_mode, p)| { - let path = Path::new(&p.install_path); - if path.exists() { - let title = path.file_name(); - Some(HeroicGameType::Heroic { - title: title.unwrap_or_default().to_string_lossy().to_string(), - app_name: p.app_name.clone(), - install_mode: **install_mode, - }) - } else { - None - } - }); - gog_shortcuts.extend(heroic_games); - - let game_folders = gog_paths - .iter() - .filter(|(_, p)| !settings.is_heroic_launch(&p.app_name)) - .filter_map(|(_, p)| { - let path = Path::new(&p.install_path); - if path.exists() { - Some(path.to_path_buf()) - } else { - None - } - }) - .collect(); - let direct_shortcuts = get_shortcuts_from_game_folders(game_folders); - for shortcut in direct_shortcuts { - let is_windows = is_windows_map.get(&shortcut.game_id).unwrap_or(&false); - gog_shortcuts.push(HeroicGameType::Gog(shortcut, *is_windows)); - } - - Ok(gog_shortcuts) -} diff --git a/src/itch/settings.rs b/src/itch/settings.rs deleted file mode 100644 index 5403fe7..0000000 --- a/src/itch/settings.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ItchSettings { - pub enabled: bool, - pub location: Option, - #[cfg(target_family = "unix")] - pub create_symlinks: bool, -} diff --git a/src/legendary/legendary_platform.rs b/src/legendary/legendary_platform.rs deleted file mode 100644 index eb8cd7f..0000000 --- a/src/legendary/legendary_platform.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::{LegendaryGame, LegendarySettings}; -use crate::platform::{Platform, SettingsValidity}; -use serde_json::from_str; -use std::error::Error; -use std::process::Command; - -pub struct LegendaryPlatform { - settings: LegendarySettings, -} - -impl LegendaryPlatform { - pub fn new(settings: LegendarySettings) -> LegendaryPlatform { - Self { settings } - } -} - -impl Platform> for LegendaryPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Legendary" - } - - fn get_shortcuts(&self) -> Result, Box> { - let legendary_string = self - .settings - .executable - .clone() - .unwrap_or_else(|| "legendary".to_string()); - let legendary = legendary_string.as_str(); - execute_legendary_command(legendary) - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: format!("{}", err), - }, - } - } - - fn needs_proton(&self, _input: &LegendaryGame) -> bool { - false - } -} - -fn execute_legendary_command(program: &str) -> Result, Box> { - let legendary_command = Command::new(program) - .arg("list-installed") - .arg("--json") - .output()?; - let json = String::from_utf8_lossy(&legendary_command.stdout); - let legendary_ouput = from_str(&json)?; - Ok(legendary_ouput) -} diff --git a/src/legendary/mod.rs b/src/legendary/mod.rs deleted file mode 100644 index 9641009..0000000 --- a/src/legendary/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod legendary_game; -mod legendary_platform; -mod settings; - -pub use legendary_game::*; -pub use legendary_platform::*; -pub use settings::*; diff --git a/src/legendary/settings.rs b/src/legendary/settings.rs deleted file mode 100644 index aa76e4a..0000000 --- a/src/legendary/settings.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct LegendarySettings { - pub enabled: bool, - pub executable: Option, -} diff --git a/src/lutris/lutris_platform.rs b/src/lutris/lutris_platform.rs deleted file mode 100644 index 5e7f4b6..0000000 --- a/src/lutris/lutris_platform.rs +++ /dev/null @@ -1,72 +0,0 @@ -use super::game_list_parser::parse_lutris_games; -use super::lutris_game::LutrisGame; -use super::settings::LutrisSettings; -use crate::platform::{Platform, SettingsValidity}; -use std::error::Error; -use std::process::Command; - -pub struct LutrisPlatform { - pub settings: LutrisSettings, -} - -impl Platform> for LutrisPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Lutris" - } - - fn get_shortcuts(&self) -> Result, Box> { - let output = get_lutris_command_output(&self.settings)?; - let games = parse_lutris_games(output.as_str()); - let mut res = vec![]; - for mut game in games { - if game.runner != "steam" { - game.settings = Some(self.settings.clone()); - res.push(game); - } - } - Ok(res) - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: format!("{}", err), - }, - } - } - - fn needs_proton(&self, _input: &LutrisGame) -> bool { - false - } -} - -fn get_lutris_command_output(settings: &LutrisSettings) -> Result> { - let output = if settings.flatpak { - let flatpak_image = &settings.flatpak_image; - #[cfg(not(feature = "flatpak"))]{ - let mut command = Command::new("flatpak"); - command.arg("run").arg(flatpak_image).arg("-lo").arg("--json").output()? - } - #[cfg(feature = "flatpak")] - { - let mut command = Command::new("flatpak-spawn"); - command.arg("--host").arg("flatpak").arg("run").arg(flatpak_image).arg("-lo").arg("--json").output()? - } - } else { - let mut command = Command::new(&settings.executable); - command.arg("-lo").arg("--json").output()? - }; - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} diff --git a/src/lutris/mod.rs b/src/lutris/mod.rs deleted file mode 100644 index eacdc4f..0000000 --- a/src/lutris/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod game_list_parser; -pub mod lutris_game; -pub mod lutris_platform; -pub mod settings; diff --git a/src/lutris/settings.rs b/src/lutris/settings.rs deleted file mode 100644 index ea58b69..0000000 --- a/src/lutris/settings.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct LutrisSettings { - pub enabled: bool, - pub executable: String, - pub flatpak: bool, - pub flatpak_image: String, -} diff --git a/src/main.rs b/src/main.rs index 4a60c77..08f48cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,16 @@ -mod amazon; mod config; -mod egs; -mod flatpak; -mod gog; -#[cfg(target_family = "unix")] -mod heroic; -mod itch; -mod legendary; -mod lutris; mod migration; -mod origin; -mod platform; +mod platforms; mod settings; mod steam; mod steamgriddb; mod sync; mod ui; -mod uplay; -mod bottles; -fn main() { +use color_eyre::eyre::Result; + +fn main() -> Result<()>{ + color_eyre::install()?; ensure_config_folder(); migration::migrate_config(); @@ -29,6 +20,7 @@ fn main() { } else { ui::run_ui(args); } + Ok(()) } fn ensure_config_folder() { diff --git a/src/migration.rs b/src/migration.rs index dee468e..e859afa 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -1,5 +1,7 @@ use std::path::Path; +use crate::{settings::save_settings, platforms::get_platforms}; + pub fn migrate_config() { let version = &crate::settings::Settings::new() .map(|s| s.config_version) @@ -35,7 +37,8 @@ pub fn migrate_config() { if save_version { if let Ok(mut settings) = crate::settings::Settings::new() { settings.config_version = Some(1); - crate::ui::MyEguiApp::save_settings_to_file(&settings); + let platforms = get_platforms(); + save_settings(&settings, &platforms); } } } diff --git a/src/origin/mod.rs b/src/origin/mod.rs deleted file mode 100644 index 9d472bf..0000000 --- a/src/origin/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod origin_game; -mod origin_platform; -mod settings; - -pub use origin_platform::*; -pub use settings::*; diff --git a/src/origin/settings.rs b/src/origin/settings.rs deleted file mode 100644 index a0e8fab..0000000 --- a/src/origin/settings.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] - -pub struct OriginSettings { - pub enabled: bool, -} diff --git a/src/platform.rs b/src/platform.rs deleted file mode 100644 index 623657b..0000000 --- a/src/platform.rs +++ /dev/null @@ -1,25 +0,0 @@ -use steam_shortcuts_util::shortcut::ShortcutOwned; - -pub trait Platform -where - T: Into, -{ - fn enabled(&self) -> bool; - - fn name(&self) -> &str; - - fn get_shortcuts(&self) -> Result, E>; - - fn settings_valid(&self) -> SettingsValidity; - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool; - - // HOME/.local/share/Steam/config/config.vdf - fn needs_proton(&self, input: &T) -> bool; -} - -pub enum SettingsValidity { - Valid, - Invalid { reason: String }, -} diff --git a/src/platforms/amazon/amazon_platform.rs b/src/platforms/amazon/amazon_platform.rs new file mode 100644 index 0000000..caec53c --- /dev/null +++ b/src/platforms/amazon/amazon_platform.rs @@ -0,0 +1,154 @@ +use serde::{Deserialize, Serialize}; +use sqlite::State; +use std::path::{Path, PathBuf}; +use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; + +use crate::platforms::{to_shortcuts_simple, ShortcutToImport, FromSettingsString, load_settings, GamesPlatform}; + +#[derive(Clone)] +pub struct AmazonPlatform { + pub settings: AmazonSettings, +} + + +impl FromSettingsString for AmazonPlatform{ + fn from_settings_string>(s:S) -> Self { + AmazonPlatform { settings: load_settings(s) } + } +} + +fn get_sqlite_path() -> eyre::Result { + let localdata = std::env::var("LOCALAPPDATA")?; + let path = Path::new(&localdata) + .join("Amazon Games") + .join("Data") + .join("Games") + .join("Sql") + .join("GameInstallInfo.sqlite"); + if path.exists() { + Ok(path) + } else { + Err(eyre::format_err!( + "Amazong GameInstallInfo.sqlite not found at {:?}", + path + )) + } +} + +fn get_launcher_path() -> eyre::Result { + let localdata = std::env::var("LOCALAPPDATA")?; + let path = Path::new(&localdata) + .join("Amazon Games") + .join("App") + .join("Amazon Games.exe"); + if path.exists() { + Ok(path) + } else { + Err(eyre::format_err!( + "Could not find Amazon Games.exe at {:?}", + path + )) + } +} + +impl GamesPlatform for AmazonPlatform{ + fn name(&self) -> &str { + "Amazon" + } + + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(self.get_amazon_games()) + + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Amazon"); + ui.checkbox(&mut self.settings.enabled, "Import from Amazon"); + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "amazon" + } +} + + +impl AmazonPlatform { + + fn get_amazon_games(&self) -> eyre::Result> { + let sqllite_path = get_sqlite_path()?; + let launcher_path = get_launcher_path()?; + let mut result = vec![]; + let connection = sqlite::open(sqllite_path)?; + let mut statement = + connection.prepare("SELECT Id, ProductTitle FROM DbSet WHERE Installed = 1")?; + while let State::Row = statement.next().unwrap() { + let id = statement.read::(0); + let title = statement.read::(1); + if let (Ok(id), Ok(title)) = (id, title) { + result.push(AmazonGame { + title, + id, + launcher_path: launcher_path.clone(), + }); + } + } + Ok(result) + } +} + +#[derive(Debug, Clone)] +pub struct AmazonGame { + pub title: String, + pub id: String, + pub launcher_path: PathBuf, +} + +impl From for ShortcutOwned { + fn from(game: AmazonGame) -> Self { + let launch = format!("amazon-games://play/{}", game.id); + let exe = game.launcher_path.to_string_lossy().to_string(); + let start_dir = game + .launcher_path + .parent() + .unwrap_or_else(|| Path::new("")) + .to_string_lossy() + .to_string(); + Shortcut::new( + "0", + game.title.as_str(), + exe.as_str(), + start_dir.as_str(), + "", + "", + launch.as_str(), + ) + .to_owned() + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AmazonSettings { + pub enabled: bool, + pub launcher_location: Option, +} + +impl Default for AmazonSettings{ + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = false; + + #[cfg(not(target_family = "unix"))] + let enabled = true; + + Self { enabled, launcher_location: Default::default() } + } +} \ No newline at end of file diff --git a/src/platforms/amazon/mod.rs b/src/platforms/amazon/mod.rs new file mode 100644 index 0000000..53eac0a --- /dev/null +++ b/src/platforms/amazon/mod.rs @@ -0,0 +1,4 @@ +mod amazon_platform; + +pub use amazon_platform::AmazonPlatform; +pub use amazon_platform::AmazonGame; \ No newline at end of file diff --git a/src/platforms/bottles/mod.rs b/src/platforms/bottles/mod.rs new file mode 100644 index 0000000..f4c156c --- /dev/null +++ b/src/platforms/bottles/mod.rs @@ -0,0 +1,3 @@ +mod platform; + +pub use platform::BottlesPlatform; diff --git a/src/bottles/platform.rs b/src/platforms/bottles/platform.rs similarity index 52% rename from src/bottles/platform.rs rename to src/platforms/bottles/platform.rs index dccad5c..e6f69fe 100644 --- a/src/bottles/platform.rs +++ b/src/platforms/bottles/platform.rs @@ -1,24 +1,46 @@ -use std::{error::Error, process::Command}; +use std::process::Command; use eframe::epaint::ahash::HashMap; use serde::{Deserialize, Serialize}; -use crate::platform::Platform; - -use super::BottlesSettings; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; +use crate::platforms::{to_shortcuts_simple, ShortcutToImport, FromSettingsString, load_settings, GamesPlatform}; + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct BottlesPlatform { pub settings: BottlesSettings, } +impl FromSettingsString for BottlesPlatform{ + fn from_settings_string>(s: S) -> Self { + BottlesPlatform { settings: load_settings(s) } + } +} + #[derive(Debug, Clone)] pub struct BottlesApp { pub name: String, pub bottle: String, } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct BottlesSettings { + pub enabled: bool, +} + +impl Default for BottlesSettings{ + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = true; + + #[cfg(not(target_family = "unix"))] + let enabled = false; + + Self { enabled } + } +} + impl From for ShortcutOwned { fn from(app: BottlesApp) -> Self { // @@ -30,65 +52,17 @@ impl From for ShortcutOwned { } } -impl Platform> for BottlesPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Bottles" - } - - fn get_shortcuts(&self) -> Result, Box> { - let mut res = vec![]; - let bottles = get_bottles(); - dbg!(&bottles); - for bottle in bottles { - for (_id,program) in bottle.external_programs { - res.push(BottlesApp{ - name:program.name, - bottle: bottle.name.clone() - }) - } - } - Ok(res) - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - if let Ok(s) =self.get_shortcuts(){ - if !s.is_empty() { - return crate::platform::SettingsValidity::Valid; - } - } - crate::platform::SettingsValidity::Invalid { reason: String::from("Nothing found")} - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn needs_proton(&self, _input: &BottlesApp) -> bool { - false +fn get_bottles() -> eyre::Result> { + let json = get_bottles_output()?; + let map: HashMap = serde_json::from_str(json.as_str())?; + let mut res = vec![]; + for (_, value) in map { + res.push(value); } + Ok(res) } -fn get_bottles() -> Vec { - let output_result = get_bottles_output(); - match output_result { - Ok(json) => { - let map : HashMap= serde_json::from_str(json.as_str()).unwrap_or_default(); - let mut res = vec![]; - for (_,value) in map{ - res.push(value); - } - res - }, - Err(_err) => vec![], - } -} - -fn get_bottles_output() -> Result> { +fn get_bottles_output() -> eyre::Result { let output = { #[cfg(not(feature = "flatpak"))] { @@ -120,17 +94,60 @@ fn get_bottles_output() -> Result> { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } -#[derive(Deserialize,Debug)] +#[derive(Deserialize, Debug)] struct Bottle { #[serde(alias = "Name")] pub(crate) name: String, #[serde(alias = "External_Programs")] - pub(crate) external_programs : HashMap, + pub(crate) external_programs: HashMap, } -#[derive(Deserialize,Debug)] +#[derive(Deserialize, Debug)] struct Program { #[serde(alias = "Name")] pub(crate) name: String, } +impl BottlesPlatform { + fn get_botttles(&self) -> eyre::Result> { + let mut res = vec![]; + let bottles = get_bottles()?; + for bottle in bottles { + for (_id, program) in bottle.external_programs { + res.push(BottlesApp { + name: program.name, + bottle: bottle.name.clone(), + }) + } + } + Ok(res) + } +} + + +impl GamesPlatform for BottlesPlatform{ + fn name(&self) -> &str { + "Bottles" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(self.get_botttles()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Bottles"); + ui.checkbox(&mut self.settings.enabled, "Import from Bottles"); + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "bottles" + } +} \ No newline at end of file diff --git a/src/platforms/egs/epic_platform.rs b/src/platforms/egs/epic_platform.rs new file mode 100644 index 0000000..0fb064b --- /dev/null +++ b/src/platforms/egs/epic_platform.rs @@ -0,0 +1,59 @@ +use crate::platforms::{ + load_settings, to_shortcuts, FromSettingsString, NeedsPorton, ShortcutToImport, GamesPlatform, +}; + +use super::{get_egs_manifests, settings::EpicGamesLauncherSettings, ManifestItem}; + +#[derive(Clone)] +pub struct EpicPlatform { + pub(crate) settings: EpicGamesLauncherSettings, + pub(crate) epic_manifests: Option>, +} + +impl FromSettingsString for EpicPlatform { + fn from_settings_string>(s: S) -> Self { + EpicPlatform { + settings: load_settings(s), + epic_manifests: None, + } + } +} + +impl NeedsPorton for ManifestItem { + fn needs_proton(&self, _platform: &EpicPlatform) -> bool { + #[cfg(target_family = "unix")] + return true; + #[cfg(target_os = "windows")] + return false; + } + + fn create_symlinks(&self, _platform: &EpicPlatform) -> bool { + false + } +} + +impl GamesPlatform for EpicPlatform{ + fn name(&self) -> &str { + "Epic" + } + + fn code_name(&self) -> &str { + "epic_games" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, get_egs_manifests(&self.settings)) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + self.render_epic_settings(ui) + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } +} \ No newline at end of file diff --git a/src/platforms/egs/epic_ui.rs b/src/platforms/egs/epic_ui.rs new file mode 100644 index 0000000..dc54017 --- /dev/null +++ b/src/platforms/egs/epic_ui.rs @@ -0,0 +1,40 @@ +use super::{get_manifests::get_egs_manifests, EpicPlatform}; + +impl EpicPlatform { + pub fn render_epic_settings(&mut self, ui: &mut egui::Ui) { + let epic_settings = &mut self.settings; + ui.heading("Epic Games"); + ui.checkbox(&mut epic_settings.enabled, "Import from Epic Games"); + if epic_settings.enabled { + let safe_mode_header = match epic_settings.safe_launch.len() { + 0 => "Force games to launch through Epic Launcher".to_string(), + 1 => "One game forced to launch through Epic Launcher".to_string(), + x => format!("{} games forced to launch through Epic Launcher", x), + }; + + egui::CollapsingHeader::new(safe_mode_header) + .id_source("Epic_Launcher_safe_launch") + .show(ui, |ui| { + ui.label("Some games must be started from the Epic Launcher, select those games below and BoilR will create shortcuts that opens the games through the Epic Launcher."); + let manifests =self.epic_manifests.get_or_insert_with(||{ + let manifests = get_egs_manifests(epic_settings); + manifests.unwrap_or_default() + }); + let mut safe_open_games = epic_settings.safe_launch.clone(); + for manifest in manifests{ + let key = manifest.get_key(); + let display_name = &manifest.display_name; + let mut safe_open = safe_open_games.contains(display_name) || safe_open_games.contains(&key); + if ui.checkbox(&mut safe_open, display_name).clicked(){ + if safe_open{ + safe_open_games.push(key); + }else{ + safe_open_games.retain(|m| m!= display_name && m!= &key); + } + } + } + epic_settings.safe_launch = safe_open_games; + }); + } + } +} diff --git a/src/egs/example_item.json b/src/platforms/egs/example_item.json similarity index 100% rename from src/egs/example_item.json rename to src/platforms/egs/example_item.json diff --git a/src/egs/get_manifests.rs b/src/platforms/egs/get_manifests.rs similarity index 74% rename from src/egs/get_manifests.rs rename to src/platforms/egs/get_manifests.rs index 423e699..edbefc0 100644 --- a/src/egs/get_manifests.rs +++ b/src/platforms/egs/get_manifests.rs @@ -1,4 +1,5 @@ -use super::{EpicGamesLauncherSettings, ManifestItem}; +use super::settings::EpicGamesLauncherSettings; +use super::ManifestItem; use std::fs::{DirEntry, File}; use std::io::BufReader; @@ -6,8 +7,8 @@ use std::path::Path; pub(crate) fn get_egs_manifests( settings: &EpicGamesLauncherSettings, -) -> Result, String> { - let locations = crate::egs::get_locations(); +) -> eyre::Result> { + let locations = super::get_locations(); match locations { Some(locations) => { let manifest_dir_path = locations.manifest_folder_path; @@ -15,14 +16,12 @@ pub(crate) fn get_egs_manifests( match manifest_dir_result { Ok(manifest_dir) => { - let all_manifests = manifest_dir + let mut all_manifests = manifest_dir .filter_map(|dir| dir.ok()) .filter_map(get_manifest_item); - let mut manifests = vec![]; - for mut manifest in all_manifests { - manifest.manifest_location = manifest.manifest_location; - #[cfg(target_family = "unix")] - { + #[cfg(target_family = "unix")] + { + for mut manifest in &mut all_manifests { if let Some(compat_folder) = locations.compat_folder_path.as_ref() { //Strip off the c:\\ manifest.manifest_location = compat_folder @@ -42,23 +41,14 @@ pub(crate) fn get_egs_manifests( .to_string(); } } - if is_game_installed(&manifest) && is_game_launchable(&manifest) { - manifests.push(manifest); - } } + let mut manifests :Vec = all_manifests + .filter(is_game_installed) + .filter(is_game_launchable) + .collect(); - manifests.sort_by_key(|m| { - format!( - "{}-{}-{}", - m.install_location, m.launch_executable, m.is_managed - ) - }); - manifests.dedup_by_key(|m| { - format!( - "{}-{}-{}", - m.install_location, m.launch_executable, m.is_managed - ) - }); + manifests.sort_by_key(|m| m.dedupe_key()); + manifests.dedup_by_key(|m| m.dedupe_key()); for mut manifest in &mut manifests { manifest.launcher_path = Some(locations.launcher_path.clone()); manifest.compat_folder = locations.compat_folder_path.clone(); @@ -70,16 +60,18 @@ pub(crate) fn get_egs_manifests( } Ok(manifests) } - Err(err) => Err(format!( + Err(err) => Err(eyre::format_err!( "Could not read dir at: {:?} error: {:?}", - manifest_dir_path, err + manifest_dir_path, + err )), } } - None => Err("Manifests not found".to_string()), + None => Err(eyre::format_err!("Manifests not found")), } } + fn is_game_installed(manifest: &ManifestItem) -> bool { Path::new(manifest.manifest_location.as_str()).exists() } diff --git a/src/egs/manifest_item.rs b/src/platforms/egs/manifest_item.rs similarity index 97% rename from src/egs/manifest_item.rs rename to src/platforms/egs/manifest_item.rs index 01b85aa..c3ec7be 100644 --- a/src/egs/manifest_item.rs +++ b/src/platforms/egs/manifest_item.rs @@ -124,6 +124,7 @@ fn launcher_shortcut(manifest: ManifestItem) -> ShortcutOwned { .to_owned() } + impl From for ShortcutOwned { fn from(manifest: ManifestItem) -> Self { let mut owned_shortcut = if manifest.needs_launcher() { @@ -162,6 +163,13 @@ impl ManifestItem { self.catalog_namespace, self.catalog_item_id, self.app_name ) } + + pub fn dedupe_key(&self)-> String { + format!( + "{}-{}-{}", + self.install_location, self.launch_executable, self.is_managed + ) + } fn needs_launcher(&self) -> bool { if self.safe_launch { diff --git a/src/egs/mod.rs b/src/platforms/egs/mod.rs similarity index 74% rename from src/egs/mod.rs rename to src/platforms/egs/mod.rs index 68324c7..26e01b1 100644 --- a/src/egs/mod.rs +++ b/src/platforms/egs/mod.rs @@ -3,9 +3,10 @@ mod get_manifests; mod manifest_item; mod paths; mod settings; +mod epic_ui; pub use epic_platform::*; use get_manifests::get_egs_manifests; pub(crate) use manifest_item::*; -pub use paths::*; -pub use settings::EpicGamesLauncherSettings; +use paths::*; + diff --git a/src/egs/paths.rs b/src/platforms/egs/paths.rs similarity index 100% rename from src/egs/paths.rs rename to src/platforms/egs/paths.rs diff --git a/src/egs/settings.rs b/src/platforms/egs/settings.rs similarity index 50% rename from src/egs/settings.rs rename to src/platforms/egs/settings.rs index 40b4494..40f9d69 100644 --- a/src/egs/settings.rs +++ b/src/platforms/egs/settings.rs @@ -5,3 +5,12 @@ pub struct EpicGamesLauncherSettings { pub enabled: bool, pub safe_launch: Vec, } + +impl Default for EpicGamesLauncherSettings { + fn default() -> Self { + Self { + enabled: true, + safe_launch: Default::default(), + } + } +} diff --git a/src/bottles/mod.rs b/src/platforms/flatpak/mod.rs similarity index 64% rename from src/bottles/mod.rs rename to src/platforms/flatpak/mod.rs index fc511aa..0ee6e29 100644 --- a/src/bottles/mod.rs +++ b/src/platforms/flatpak/mod.rs @@ -2,4 +2,4 @@ mod platform; mod settings; pub use platform::*; -pub use settings::*; +pub(crate) use settings::*; diff --git a/src/flatpak/platform.rs b/src/platforms/flatpak/platform.rs similarity index 58% rename from src/flatpak/platform.rs rename to src/platforms/flatpak/platform.rs index b4c8009..34c1646 100644 --- a/src/flatpak/platform.rs +++ b/src/platforms/flatpak/platform.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::platform::{Platform, SettingsValidity}; -use std::error::Error; +use crate::platforms::{NeedsPorton, to_shortcuts, ShortcutToImport, GamesPlatform, FromSettingsString, load_settings}; use super::FlatpakSettings; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; @@ -24,16 +23,19 @@ impl From for ShortcutOwned { } } -impl Platform> for FlatpakPlatform { - fn enabled(&self) -> bool { - self.settings.enabled +impl NeedsPorton for FlatpakApp{ + fn needs_proton(&self, _platform: &FlatpakPlatform) -> bool { + false } - fn name(&self) -> &str { - "Flatpak" + fn create_symlinks(&self, _platform: &FlatpakPlatform) -> bool { + false } +} - fn get_shortcuts(&self) -> Result, Box> { +impl FlatpakPlatform { + + fn get_flatpak_apps(&self) -> eyre::Result> { use std::process::Command; let mut command = Command::new("flatpak"); let output = command @@ -56,23 +58,41 @@ impl Platform> for FlatpakPlatform { } Ok(result) } +} - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: format!("{}", err), - }, + +impl FromSettingsString for FlatpakPlatform{ + fn from_settings_string>(s: S) -> Self { + FlatpakPlatform { + settings: load_settings(s), } } +} - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false +impl GamesPlatform for FlatpakPlatform{ + fn name(&self) -> &str { + "Flatpak" + } + + + fn enabled(&self) -> bool { + self.settings.enabled } - fn needs_proton(&self, _input: &FlatpakApp) -> bool { - false + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, self.get_flatpak_apps()) } -} + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Flatpak"); + ui.checkbox(&mut self.settings.enabled, "Import from Flatpak"); + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "flatpak" + } +} \ No newline at end of file diff --git a/src/platforms/flatpak/settings.rs b/src/platforms/flatpak/settings.rs new file mode 100644 index 0000000..e455b42 --- /dev/null +++ b/src/platforms/flatpak/settings.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct FlatpakSettings { + pub enabled: bool, +} + +impl Default for FlatpakSettings{ + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = true; + + #[cfg(not(target_family = "unix"))] + let enabled = false; + + Self { enabled } + } +} \ No newline at end of file diff --git a/src/gog/gog_config.rs b/src/platforms/gog/gog_config.rs similarity index 100% rename from src/gog/gog_config.rs rename to src/platforms/gog/gog_config.rs diff --git a/src/gog/gog_game.rs b/src/platforms/gog/gog_game.rs similarity index 100% rename from src/gog/gog_game.rs rename to src/platforms/gog/gog_game.rs diff --git a/src/gog/gog_platform.rs b/src/platforms/gog/gog_platform.rs similarity index 73% rename from src/gog/gog_platform.rs rename to src/platforms/gog/gog_platform.rs index 247ddf3..36b7589 100644 --- a/src/gog/gog_platform.rs +++ b/src/platforms/gog/gog_platform.rs @@ -1,20 +1,44 @@ use std::path::{Path, PathBuf}; -use crate::{gog::gog_config::GogConfig, platform::Platform}; +use crate::platforms::{to_shortcuts, ShortcutToImport, NeedsPorton, GamesPlatform, FromSettingsString, load_settings}; use super::{ + gog_config::GogConfig, gog_game::{GogGame, GogShortcut}, GogSettings, }; +#[derive(Clone)] pub struct GogPlatform { pub settings: GogSettings, } -pub fn get_shortcuts_from_config( +impl GogPlatform { + fn get_shortcuts(&self) -> eyre::Result> { + let gog_location = self + .settings + .location + .as_ref() + .map(|location| Path::new(&location).to_path_buf()) + .unwrap_or_else(default_location); + if !gog_location.exists() { + return Err(eyre::format_err!("Could not find path: {:?}", gog_location)); + } + let config_path = gog_location.join("config.json"); + if !config_path.exists() { + return Err(eyre::format_err!( + "Config file not found: {:?}", + config_path + )); + } + get_shortcuts_from_config(self.settings.wine_c_drive.clone(), config_path) + } +} + +fn get_shortcuts_from_config( _wine_c_drive: Option, config_path: PathBuf, -) -> Result, String> { +) -> eyre::Result> { let install_locations = get_install_locations(config_path)?; #[cfg(target_family = "unix")] let install_locations = if let Some(wine_c_drive) = &_wine_c_drive { @@ -38,11 +62,11 @@ pub fn get_shortcuts_from_config( } } } - let shortcuts = get_shortcuts_from_game_folders(game_folders); + let shortcuts = get_gog_shortcuts_from_game_folders(game_folders); Ok(shortcuts) } -pub fn get_shortcuts_from_game_folders(game_folders: Vec) -> Vec { +pub fn get_gog_shortcuts_from_game_folders(game_folders: Vec) -> Vec { let games = get_games_from_game_folders(game_folders); get_shortcuts_from_games(games) @@ -137,53 +161,25 @@ fn get_games_from_game_folders(game_folders: Vec) -> Vec<(GogGame, Path games } -impl Platform for GogPlatform { - fn enabled(&self) -> bool { - self.settings.enabled +impl NeedsPorton for GogShortcut{ + #[cfg(target_family = "unix")] + fn needs_proton(&self, _platform: &GogPlatform) -> bool { + true } - fn name(&self) -> &str { - "Gog" + #[cfg(not(target_family = "unix"))] + fn needs_proton(&self, _platform: &GogPlatform) -> bool { + false + } + + #[cfg(not(target_family = "unix"))] + fn create_symlinks(&self, _platform: &GogPlatform) -> bool { + false } #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - self.settings.create_symlinks - } - - fn get_shortcuts(&self) -> Result, String> { - let gog_location = self - .settings - .location - .as_ref() - .map(|location| Path::new(&location).to_path_buf()) - .unwrap_or_else(default_location); - if !gog_location.exists() { - return Err(format!("Could not find path: {:?}", gog_location)); - } - let config_path = gog_location.join("config.json"); - if !config_path.exists() { - return Err(format!("Config file not found: {:?}", config_path)); - } - get_shortcuts_from_config(self.settings.wine_c_drive.clone(), config_path) - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - use crate::platform::*; - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: err.to_string(), - }, - } - } - - fn needs_proton(&self, _input: &GogShortcut) -> bool { - #[cfg(target_family = "unix")] - return true; - #[cfg(target_os = "windows")] - return false; + fn create_symlinks(&self, platform: &GogPlatform) -> bool { + platform.settings.create_symlinks } } @@ -202,11 +198,9 @@ fn fix_paths(wine_c_drive: &str, paths: Vec) -> Vec { .collect() } -fn get_install_locations(path: PathBuf) -> Result, String> { - let data_res = std::fs::read_to_string(&path) - .map_err(|e| format!("Config file not read {:?} , error: {:?} ", path.clone(), e))?; - let config: GogConfig = serde_json::from_str(&data_res) - .map_err(|e| format!("Config file not read {:?} , error: {:?} ", path.clone(), e))?; +fn get_install_locations(path: PathBuf) -> eyre::Result> { + let data_res = std::fs::read_to_string(&path)?; + let config: GogConfig = serde_json::from_str(&data_res)?; let path_vec = match config.library_path { Some(path) => vec![path], None => vec![], @@ -227,3 +221,49 @@ pub fn default_location() -> PathBuf { Path::new(&home).join("Games/gog-galaxy/drive_c/ProgramData/GOG.com/Galaxy") } } + + +impl FromSettingsString for GogPlatform{ + fn from_settings_string>(s: S) -> Self { + GogPlatform { + settings: load_settings(s), + } + } +} + +impl GamesPlatform for GogPlatform{ + fn name(&self) -> &str { + "GOG" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, self.get_shortcuts()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("GoG Galaxy"); + ui.checkbox(&mut self.settings.enabled, "Import from GoG Galaxy"); + if self.settings.enabled { + ui.horizontal(|ui| { + let mut empty_string = "".to_string(); + let gog_location = self.settings.location.as_mut().unwrap_or(&mut empty_string); + ui.label("GoG Galaxy Folder: "); + if ui.text_edit_singleline(gog_location).changed() { + self.settings.location = Some(gog_location.to_string()); + } + }); + } + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "gog" + } +} \ No newline at end of file diff --git a/src/platforms/gog/gog_settings.rs b/src/platforms/gog/gog_settings.rs new file mode 100644 index 0000000..7c2d485 --- /dev/null +++ b/src/platforms/gog/gog_settings.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GogSettings { + pub enabled: bool, + pub location: Option, + pub wine_c_drive: Option, + #[cfg(target_family = "unix")] + pub create_symlinks: bool, +} + +impl Default for GogSettings { + fn default() -> Self { + Self { + #[cfg(target_family = "unix")] + enabled: false, + #[cfg(not(target_family = "unix"))] + enabled: true, + location:None, + wine_c_drive: None, + #[cfg(target_family = "unix")] + create_symlinks: true, + } + } +} diff --git a/src/platforms/gog/mod.rs b/src/platforms/gog/mod.rs new file mode 100644 index 0000000..6f76146 --- /dev/null +++ b/src/platforms/gog/mod.rs @@ -0,0 +1,9 @@ +mod gog_config; +mod gog_game; +mod gog_platform; +mod gog_settings; + +pub(crate) use gog_settings::GogSettings; +pub use gog_platform::get_gog_shortcuts_from_game_folders; +pub use gog_platform::GogPlatform; +pub use gog_game::GogShortcut; \ No newline at end of file diff --git a/src/heroic/heroic_game.rs b/src/platforms/heroic/heroic_game.rs similarity index 100% rename from src/heroic/heroic_game.rs rename to src/platforms/heroic/heroic_game.rs diff --git a/src/heroic/heroic_game_type.rs b/src/platforms/heroic/heroic_game_type.rs similarity index 97% rename from src/heroic/heroic_game_type.rs rename to src/platforms/heroic/heroic_game_type.rs index 7e94f58..cd41319 100644 --- a/src/heroic/heroic_game_type.rs +++ b/src/platforms/heroic/heroic_game_type.rs @@ -1,13 +1,12 @@ use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use super::{HeroicGame, InstallationMode}; -use crate::gog::GogShortcut; #[derive(Clone)] pub enum HeroicGameType { Epic(HeroicGame), //The bool is if it is windows (true) or not (false) - Gog(GogShortcut, bool), + Gog(crate::platforms::GogShortcut, bool), //The string is the app name Heroic { title: String, diff --git a/src/platforms/heroic/heroic_platform.rs b/src/platforms/heroic/heroic_platform.rs new file mode 100644 index 0000000..cba5c28 --- /dev/null +++ b/src/platforms/heroic/heroic_platform.rs @@ -0,0 +1,285 @@ +use serde::Deserialize; + +use super::{HeroicGame, HeroicGameType, HeroicSettings}; +use crate::platforms::{GamesPlatform, FromSettingsString, load_settings}; +use crate::platforms::{to_shortcuts, NeedsPorton, ShortcutToImport}; +use std::collections::HashMap; +use std::path::Path; + +use std::path::PathBuf; + +#[derive(Clone)] +pub struct HeroicPlatform { + pub settings: HeroicSettings, + pub(crate) heroic_games: Option>, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +pub enum InstallationMode { + FlatPak, + UserBin, +} + +#[derive(Deserialize)] +struct HeroicGogConfig { + installed: Vec, +} + +#[derive(Deserialize)] +struct HeroicGogPath { + platform: String, + #[serde(alias = "appName")] + app_name: String, + install_path: String, +} + +fn get_installed_json_location(install_mode: &InstallationMode) -> PathBuf { + let home_dir = std::env::var("HOME").unwrap_or_else(|_| "".to_string()); + match install_mode { + InstallationMode::FlatPak => Path::new(&home_dir) + .join(".var/app/com.heroicgameslauncher.hgl/config/legendary/installed.json"), + InstallationMode::UserBin => Path::new(&home_dir).join(".config/legendary/installed.json"), + } +} + +fn get_gog_installed_location(install_mode: &InstallationMode) -> PathBuf { + let home_dir = std::env::var("HOME").unwrap_or_else(|_| "".to_string()); + match install_mode { + InstallationMode::FlatPak => Path::new(&home_dir) + .join(".var/app/com.heroicgameslauncher.hgl/config/heroic/gog_store/installed.json"), + InstallationMode::UserBin => { + Path::new(&home_dir).join(".config/heroic/gog_store/installed.json") + } + } +} + +fn get_shortcuts_from_install_mode( + install_mode: &InstallationMode, +) -> eyre::Result> { + let installed_path = get_installed_json_location(install_mode); + get_shortcuts_from_location(installed_path) +} + +fn get_shortcuts_from_location>(path: P) -> eyre::Result> { + let installed_json_path = path.as_ref(); + if installed_json_path.exists() { + let json = std::fs::read_to_string(installed_json_path)?; + let games_map = serde_json::from_str::>(&json)?; + let mut games = vec![]; + for game in games_map.values() { + games.push(game.clone()); + } + Ok(games) + } else { + Ok(vec![]) + } +} + +impl HeroicPlatform { + pub fn get_heroic_games(&self) -> eyre::Result> { + let install_modes = vec![InstallationMode::FlatPak, InstallationMode::UserBin]; + + let mut heroic_games = self.get_epic_games(&install_modes)?; + let gog_games = get_gog_games(&self.settings, &install_modes)?; + heroic_games.extend(gog_games); + Ok(heroic_games) + } +} + +impl NeedsPorton for HeroicGameType{ + #[cfg(not(target_family = "unix"))] + fn needs_proton(&self, _platform: &HeroicPlatform) -> bool { + false + } + + #[cfg(target_family = "unix")] + fn needs_proton(&self, _platform: &HeroicPlatform) -> bool { + match self { + HeroicGameType::Epic(_game) => true, + HeroicGameType::Gog(_, is_windows) => *is_windows, + HeroicGameType::Heroic { .. } => false, + } + } + + fn create_symlinks(&self, _platform: &HeroicPlatform) -> bool { + false + } +} + +impl HeroicPlatform { + pub fn get_epic_games(&self, install_modes: &[InstallationMode]) -> eyre::Result> { + let mut shortcuts = vec![]; + for install_mode in install_modes { + if let Ok(mut games) = get_shortcuts_from_install_mode(install_mode) { + games.sort_by_key(|m| { + format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name) + }); + games.dedup_by_key(|m| { + format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name) + }); + + for game in games { + if self.settings.is_heroic_launch(&game.app_name) { + shortcuts.push(HeroicGameType::Heroic { + title: game.title, + app_name: game.app_name, + install_mode: *install_mode, + }); + } else if game.is_installed() { + shortcuts.push(HeroicGameType::Epic(game)); + } + } + } + } + Ok(shortcuts) + } +} +fn get_gog_games( + settings: &HeroicSettings, + install_modes: &[InstallationMode], +) -> eyre::Result> { + let mut gog_paths = vec![]; + for install_mode in install_modes { + let config = get_gog_installed_location(install_mode); + if config.exists() { + if let Ok(config_string) = std::fs::read_to_string(config) { + if let Ok(config) = serde_json::from_str::(&config_string) { + for c in config.installed { + gog_paths.push((install_mode, c)); + } + } + } + } + } + + let mut is_windows_map = HashMap::new(); + + for (_, path) in gog_paths.iter() { + is_windows_map.insert(path.app_name.clone(), path.platform == "windows"); + } + + let mut gog_shortcuts = vec![]; + + let heroic_games = gog_paths + .iter() + .filter(|(_, p)| settings.is_heroic_launch(&p.app_name)) + .filter_map(|(install_mode, p)| { + let path = Path::new(&p.install_path); + if path.exists() { + let title = path.file_name(); + Some(HeroicGameType::Heroic { + title: title.unwrap_or_default().to_string_lossy().to_string(), + app_name: p.app_name.clone(), + install_mode: **install_mode, + }) + } else { + None + } + }); + gog_shortcuts.extend(heroic_games); + + let game_folders = gog_paths + .iter() + .filter(|(_, p)| !settings.is_heroic_launch(&p.app_name)) + .filter_map(|(_, p)| { + let path = Path::new(&p.install_path); + if path.exists() { + Some(path.to_path_buf()) + } else { + None + } + }) + .collect(); + let direct_shortcuts = crate::platforms::get_gog_shortcuts_from_game_folders(game_folders); + for shortcut in direct_shortcuts { + let is_windows = is_windows_map.get(&shortcut.game_id).unwrap_or(&false); + gog_shortcuts.push(HeroicGameType::Gog(shortcut, *is_windows)); + } + + Ok(gog_shortcuts) +} + + +impl FromSettingsString for HeroicPlatform{ + fn from_settings_string>(s: S) -> Self { + HeroicPlatform{ + heroic_games: None, + settings : load_settings(s) + } + } +} + +impl GamesPlatform for HeroicPlatform{ + fn name(&self) -> &str { + "Heroic" + } + + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, self.get_heroic_games()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Heroic"); + ui.checkbox(&mut self.settings.enabled, "Import from Heroic"); + ui.checkbox(&mut self.settings.default_launch_through_heroic, "Always launch games through Heroic"); + let safe_mode_header = match (self.settings.default_launch_through_heroic,self.settings.launch_games_through_heroic.len()) { + (false,0) => "Force games to launch through Heroic Launcher".to_string(), + (false,1) => "One game forced to launch through Heroic Launcher".to_string(), + (false,x) => format!("{} games forced to launch through Heroic Launcher", x), + + (true,0) => "Force games to launch directly".to_string(), + (true,1) => "One game forced to launch directly".to_string(), + (true,x) => format!("{} games forced to launch directly", x), + + }; + egui::CollapsingHeader::new(safe_mode_header) + .id_source("Heroic_Launcher_safe_launch") + .show(ui, |ui| { +if +self.settings.default_launch_through_heroic{ + ui.label("Some games work best when launched directly, select those games below and BoilR will create shortcuts that launch the games directly."); + + } else{ + ui.label("Some games must be started from the Heroic Launcher, select those games below and BoilR will create shortcuts that opens the games through the Heroic Launcher."); + + } + let manifests =self.heroic_games.get_or_insert_with(||{ + let heroic_setting = self.settings.clone(); + + let heroic_platform = HeroicPlatform{ + settings:heroic_setting, + heroic_games:None + }; + heroic_platform.get_heroic_games().unwrap_or_default() + }); + + let safe_open_games = &mut self.settings.launch_games_through_heroic; + for manifest in manifests{ + let key = manifest.app_name(); + let display_name = manifest.title(); + let mut safe_open = safe_open_games.contains(&display_name.to_string()) || safe_open_games.contains(&key.to_string()); + if ui.checkbox(&mut safe_open, display_name).clicked(){ + if safe_open{ + safe_open_games.push(key.to_string()); + }else{ + safe_open_games.retain(|m| m!= display_name && m!= key); + } + } + } + }) ; + + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "heroic" + } +} \ No newline at end of file diff --git a/src/heroic/mod.rs b/src/platforms/heroic/mod.rs similarity index 100% rename from src/heroic/mod.rs rename to src/platforms/heroic/mod.rs diff --git a/src/heroic/settings.rs b/src/platforms/heroic/settings.rs similarity index 61% rename from src/heroic/settings.rs rename to src/platforms/heroic/settings.rs index b1b172b..3927c21 100644 --- a/src/heroic/settings.rs +++ b/src/platforms/heroic/settings.rs @@ -19,3 +19,19 @@ impl HeroicSettings { } } } + +impl Default for HeroicSettings { + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = true; + + #[cfg(not(target_family = "unix"))] + let enabled = false; + + Self { + enabled, + launch_games_through_heroic: Default::default(), + default_launch_through_heroic: true, + } + } +} diff --git a/src/itch/butler_db_parser.rs b/src/platforms/itch/butler_db_parser.rs similarity index 95% rename from src/itch/butler_db_parser.rs rename to src/platforms/itch/butler_db_parser.rs index 03ea668..5369766 100644 --- a/src/itch/butler_db_parser.rs +++ b/src/platforms/itch/butler_db_parser.rs @@ -67,7 +67,7 @@ mod tests { #[test] fn parse_itch_butler_db_test() { - let content = include_bytes!("../testdata/itch/butler.db-wal"); + let content = include_bytes!("../../testdata/itch/butler.db-wal"); let result = parse_butler_db(content); assert!(result.is_ok()); let (_r, paths) = result.unwrap(); @@ -98,7 +98,7 @@ mod tests { #[test] fn parse_itch_butler_db_test_other() { - let content = include_bytes!("../testdata/itch/other-butler.db-wal"); + let content = include_bytes!("../../testdata/itch/other-butler.db-wal"); let result = parse_butler_db(content); assert!(result.is_ok()); let (_r, paths) = result.unwrap(); diff --git a/src/itch/itch_game.rs b/src/platforms/itch/itch_game.rs similarity index 65% rename from src/itch/itch_game.rs rename to src/platforms/itch/itch_game.rs index 79883b3..f7622bb 100644 --- a/src/itch/itch_game.rs +++ b/src/platforms/itch/itch_game.rs @@ -3,6 +3,10 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; +use crate::platforms::NeedsPorton; + +use super::ItchPlatform; + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ItchGame { @@ -33,3 +37,21 @@ impl From for ShortcutOwned { owned_shortcut } } + + + +impl NeedsPorton for ItchGame{ + fn needs_proton(&self, _platform: &ItchPlatform) -> bool { + self.executable.ends_with("exe") + } + + #[cfg(target_family = "unix")] + fn create_symlinks(&self, platform: &ItchPlatform) -> bool { + platform.settings.create_symlinks + } + + #[cfg(not(target_family = "unix"))] + fn create_symlinks(&self, _platform: &ItchPlatform) -> bool { + false + } +} \ No newline at end of file diff --git a/src/itch/itch_platform.rs b/src/platforms/itch/itch_platform.rs similarity index 50% rename from src/itch/itch_platform.rs rename to src/platforms/itch/itch_platform.rs index b69b1a5..3942b61 100644 --- a/src/itch/itch_platform.rs +++ b/src/platforms/itch/itch_platform.rs @@ -1,46 +1,37 @@ -use super::butler_db_parser::*; +use crate::platforms::{to_shortcuts, ShortcutToImport, load_settings, FromSettingsString, GamesPlatform}; +use super::itch_game::ItchGame; use super::receipt::Receipt; -use super::{ItchGame, ItchSettings}; -use crate::platform::{Platform, SettingsValidity}; +use super::ItchSettings; +use super::{butler_db_parser::*}; use flate2::read::GzDecoder; use is_executable::IsExecutable; use std::collections::HashSet; use std::io::prelude::*; use std::path::Path; +#[derive(Clone)] pub struct ItchPlatform { - settings: ItchSettings, + pub settings: ItchSettings, } impl ItchPlatform { - pub fn new(settings: ItchSettings) -> ItchPlatform { - ItchPlatform { settings } - } -} - -impl Platform for ItchPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Itch" - } - - fn get_shortcuts(&self) -> Result, String> { + fn get_itch_games(&self) -> eyre::Result> { let itch_location = self.settings.location.clone(); let itch_location = itch_location.unwrap_or_else(get_default_location); let itch_db_location = Path::new(&itch_location).join("db").join("butler.db-wal"); if !itch_db_location.exists() { - return Err(format!("Path not found: {:?}", itch_db_location.to_str())); + return Err(eyre::format_err!( + "Path not found: {:?}", + itch_db_location.to_str() + )); } let shortcut_bytes = std::fs::read(&itch_db_location).unwrap(); let paths = match parse_butler_db(&shortcut_bytes) { Ok((_, shortcuts)) => Ok(shortcuts), - Err(e) => Err(format!( + Err(e) => Err(eyre::format_err!( "Could not parse path: {:?} , error: {:?}", itch_db_location.to_str(), e @@ -52,31 +43,6 @@ impl Platform for ItchPlatform { let res = paths.iter().filter_map(|e| dbpath_to_game(*e)).collect(); Ok(res) } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - self.settings.create_symlinks - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: err.to_string(), - }, - } - } - #[cfg(target_os = "windows")] - fn needs_proton(&self, _input: &ItchGame) -> bool { - false - } - - #[cfg(target_family = "unix")] - fn needs_proton(&self, input: &ItchGame) -> bool { - //We can only really guess here - input.executable.ends_with("exe") - } } fn dbpath_to_game(paths: &DbPaths) -> Option { @@ -86,13 +52,11 @@ fn dbpath_to_game(paths: &DbPaths) -> Option { if !&recipt.exists() { return None; } - - let executable = paths + paths .paths .iter() - .find(|p| Path::new(&paths.base_path).join(&p).is_executable()); - match executable { - Some(executable) => { + .filter(|p| Path::new(&paths.base_path).join(&p).is_executable()) + .find_map(|executable| { let gz_bytes = std::fs::read(&recipt).unwrap(); let mut d = GzDecoder::new(gz_bytes.as_slice()); let mut s = String::new(); @@ -104,9 +68,7 @@ fn dbpath_to_game(paths: &DbPaths) -> Option { executable: executable.to_owned(), title: re.game.title, }) - } - None => None, - } + }) } #[cfg(target_family = "unix")] @@ -125,5 +87,70 @@ pub fn get_default_location() -> String { .to_str() .unwrap() .to_string() - //C:\Users\phili\AppData\Local\itch + //C:\Users\phili\AppData\Local\itch +} + + + +impl FromSettingsString for ItchPlatform { + fn from_settings_string>(s: S) -> Self { + ItchPlatform { + settings: load_settings(s), + } + } +} + + +impl GamesPlatform for ItchPlatform{ + fn name(&self) -> &str { + "Itch" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, self.get_itch_games()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Itch.io"); + ui.checkbox(&mut self.settings.enabled, "Import from Itch.io"); + if self.settings.enabled { + ui.horizontal(|ui| { + let mut empty_string = "".to_string(); + let itch_location = self + .settings + .location + .as_mut() + .unwrap_or(&mut empty_string); + ui.label("Itch.io Folder: "); + if ui.text_edit_singleline(itch_location).changed() { + self.settings.location = if itch_location.is_empty() { + None + } else { + Some(itch_location.to_string()) + }; + } else if !itch_location.is_empty() && ui + .button("Reset") + .on_hover_text("Reset the itch path, let BoilR guess again") + .clicked() { + self.settings.location = None; + } + }); + #[cfg(target_family = "unix")] + { + ui.checkbox(&mut self.settings.create_symlinks, "Create symlinks"); + } + } + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "itch" + } } diff --git a/src/itch/mod.rs b/src/platforms/itch/mod.rs similarity index 54% rename from src/itch/mod.rs rename to src/platforms/itch/mod.rs index 2c14c02..8895972 100644 --- a/src/itch/mod.rs +++ b/src/platforms/itch/mod.rs @@ -4,6 +4,5 @@ mod itch_platform; mod receipt; mod settings; -pub use itch_game::*; -pub use itch_platform::*; -pub use settings::*; +pub use itch_platform::ItchPlatform; +pub use settings::ItchSettings; \ No newline at end of file diff --git a/src/itch/receipt.rs b/src/platforms/itch/receipt.rs similarity index 100% rename from src/itch/receipt.rs rename to src/platforms/itch/receipt.rs diff --git a/src/platforms/itch/settings.rs b/src/platforms/itch/settings.rs new file mode 100644 index 0000000..f18cd25 --- /dev/null +++ b/src/platforms/itch/settings.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ItchSettings { + pub enabled: bool, + pub location: Option, + #[cfg(target_family = "unix")] + pub create_symlinks: bool, +} + +impl Default for ItchSettings { + fn default() -> Self { + Self { + enabled: true, + location: Default::default(), + #[cfg(target_family = "unix")] + create_symlinks: true, + } + } +} diff --git a/src/legendary/legendary_game.rs b/src/platforms/legendary/legendary_game.rs similarity index 100% rename from src/legendary/legendary_game.rs rename to src/platforms/legendary/legendary_game.rs diff --git a/src/platforms/legendary/legendary_platform.rs b/src/platforms/legendary/legendary_platform.rs new file mode 100644 index 0000000..df24098 --- /dev/null +++ b/src/platforms/legendary/legendary_platform.rs @@ -0,0 +1,82 @@ +use super::legendary_game::LegendaryGame; +use super::LegendarySettings; +use crate::platforms::{to_shortcuts_simple, GamesPlatform, ShortcutToImport, FromSettingsString, load_settings}; +use serde_json::from_str; +use std::process::Command; + +#[derive(Clone)] +pub struct LegendaryPlatform { + pub settings: LegendarySettings, +} + +impl LegendaryPlatform { + fn get_shortcuts(&self) -> eyre::Result> { + let legendary_string = self + .settings + .executable + .clone() + .unwrap_or_else(|| "legendary".to_string()); + let legendary = legendary_string.as_str(); + execute_legendary_command(legendary) + } +} + +fn execute_legendary_command(program: &str) -> eyre::Result> { + let legendary_command = Command::new(program) + .arg("list-installed") + .arg("--json") + .output()?; + let json = String::from_utf8_lossy(&legendary_command.stdout); + let games = from_str(&json)?; + Ok(games) +} + +impl GamesPlatform for LegendaryPlatform { + fn name(&self) -> &str { + "Legendary" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(self.get_shortcuts()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Legendary & Rare"); + ui.checkbox(&mut self.settings.enabled, "Import from Legendary & Rare"); + if self.settings.enabled { + ui.horizontal(|ui| { + let mut empty_string = "".to_string(); + let legendary_location = self + .settings + .executable + .as_mut() + .unwrap_or(&mut empty_string); + ui.label("Legendary Executable: ") + .on_hover_text("The location of the legendary executable to use"); + if ui.text_edit_singleline(legendary_location).changed() { + self.settings.executable = Some(legendary_location.to_string()); + } + }); + } + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "legendary" + } +} + +impl FromSettingsString for LegendaryPlatform{ + fn from_settings_string>(s: S) -> Self { + LegendaryPlatform{ + settings:load_settings(s) + } + } +} \ No newline at end of file diff --git a/src/platforms/legendary/mod.rs b/src/platforms/legendary/mod.rs new file mode 100644 index 0000000..ddb46c9 --- /dev/null +++ b/src/platforms/legendary/mod.rs @@ -0,0 +1,7 @@ +mod legendary_game; +mod legendary_platform; +mod settings; + + +pub use settings::LegendarySettings; +pub use legendary_platform::LegendaryPlatform; \ No newline at end of file diff --git a/src/platforms/legendary/settings.rs b/src/platforms/legendary/settings.rs new file mode 100644 index 0000000..155052c --- /dev/null +++ b/src/platforms/legendary/settings.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct LegendarySettings { + pub enabled: bool, + pub executable: Option, +} + +impl Default for LegendarySettings { + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = true; + #[cfg(target_family = "windows")] + let enabled = false; + Self { + enabled, + executable: Default::default(), + } + } +} diff --git a/src/lutris/game_list_parser.rs b/src/platforms/lutris/game_list_parser.rs similarity index 100% rename from src/lutris/game_list_parser.rs rename to src/platforms/lutris/game_list_parser.rs diff --git a/src/lutris/lutris_game.rs b/src/platforms/lutris/lutris_game.rs similarity index 97% rename from src/lutris/lutris_game.rs rename to src/platforms/lutris/lutris_game.rs index 5f90455..3d7eaf6 100644 --- a/src/lutris/lutris_game.rs +++ b/src/platforms/lutris/lutris_game.rs @@ -1,8 +1,9 @@ -use crate::lutris::settings::LutrisSettings; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use serde::Deserialize; +use super::LutrisSettings; + #[derive(Deserialize, Clone)] pub struct LutrisGame { pub id: i64, diff --git a/src/platforms/lutris/lutris_platform.rs b/src/platforms/lutris/lutris_platform.rs new file mode 100644 index 0000000..05adb1a --- /dev/null +++ b/src/platforms/lutris/lutris_platform.rs @@ -0,0 +1,110 @@ +use super::game_list_parser::parse_lutris_games; +use super::lutris_game::LutrisGame; +use super::settings::LutrisSettings; +use crate::platforms::{to_shortcuts_simple, ShortcutToImport, FromSettingsString, load_settings, GamesPlatform}; +use std::process::Command; + +#[derive(Clone)] +pub struct LutrisPlatform { + pub settings: LutrisSettings, +} + +impl LutrisPlatform { + fn get_shortcuts(&self) -> eyre::Result> { + let output = get_lutris_command_output(&self.settings)?; + let games = parse_lutris_games(output.as_str()); + let mut res = vec![]; + for mut game in games { + if game.runner != "steam" { + game.settings = Some(self.settings.clone()); + res.push(game); + } + } + Ok(res) + } +} + +fn get_lutris_command_output(settings: &LutrisSettings) -> eyre::Result { + let output = if settings.flatpak { + let flatpak_image = &settings.flatpak_image; + #[cfg(not(feature = "flatpak"))] + { + let mut command = Command::new("flatpak"); + command + .arg("run") + .arg(flatpak_image) + .arg("-lo") + .arg("--json") + .output()? + } + #[cfg(feature = "flatpak")] + { + let mut command = Command::new("flatpak-spawn"); + command + .arg("--host") + .arg("flatpak") + .arg("run") + .arg(flatpak_image) + .arg("-lo") + .arg("--json") + .output()? + } + } else { + let mut command = Command::new(&settings.executable); + command.arg("-lo").arg("--json").output()? + }; + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +impl FromSettingsString for LutrisPlatform{ + fn from_settings_string>(s: S) -> Self { + LutrisPlatform { settings: load_settings(s) } + } +} + +impl GamesPlatform for LutrisPlatform{ + fn name(&self) -> &str { + "Lutris" + } + + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(self.get_shortcuts()) + + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Lutris"); + ui.checkbox(&mut self.settings.enabled, "Import from Lutris"); + if self.settings.enabled { + ui.checkbox(&mut self.settings.flatpak, "Flatpak version"); + if !self.settings.flatpak { + ui.horizontal(|ui| { + let lutris_location = &mut self.settings.executable; + ui.label("Lutris Location: "); + ui.text_edit_singleline(lutris_location); + }); + } else { + ui.horizontal(|ui| { + let flatpak_image = &mut self.settings.flatpak_image; + ui.label("Flatpak image"); + ui.text_edit_singleline(flatpak_image); + }); + } + } + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "lutris" + } + +} \ No newline at end of file diff --git a/src/platforms/lutris/mod.rs b/src/platforms/lutris/mod.rs new file mode 100644 index 0000000..12f3acc --- /dev/null +++ b/src/platforms/lutris/mod.rs @@ -0,0 +1,7 @@ +mod game_list_parser; +mod lutris_game; +mod lutris_platform; +mod settings; + +pub(crate) use lutris_platform::LutrisPlatform; +pub use settings::LutrisSettings; diff --git a/src/platforms/lutris/settings.rs b/src/platforms/lutris/settings.rs new file mode 100644 index 0000000..214d181 --- /dev/null +++ b/src/platforms/lutris/settings.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct LutrisSettings { + pub enabled: bool, + pub executable: String, + pub flatpak: bool, + pub flatpak_image: String, +} + +impl Default for LutrisSettings { + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = true; + + #[cfg(not(target_family = "unix"))] + let enabled = false; + + Self { + enabled, + executable: "lutris".to_string(), + flatpak: true, + flatpak_image: "net.lutris.Lutris//beta".to_string(), + } + } +} diff --git a/src/lutris/test_output.txt b/src/platforms/lutris/test_output.txt similarity index 100% rename from src/lutris/test_output.txt rename to src/platforms/lutris/test_output.txt diff --git a/src/platforms/mod.rs b/src/platforms/mod.rs new file mode 100644 index 0000000..798e9ec --- /dev/null +++ b/src/platforms/mod.rs @@ -0,0 +1,22 @@ +mod platform; +mod egs; +mod amazon; +mod bottles; +mod uplay; +mod itch; +mod flatpak; +mod origin; +mod gog; +mod heroic; +mod lutris; +mod legendary; +mod platforms_load; + +pub(crate) use platform::*; + +pub(crate) use gog::get_gog_shortcuts_from_game_folders; +pub(crate) use gog::GogShortcut; +pub(crate) use platforms_load::FromSettingsString; +pub(crate) use platforms_load::load_settings; +pub use platforms_load::Platforms; +pub use platforms_load::get_platforms; diff --git a/src/platforms/origin/mod.rs b/src/platforms/origin/mod.rs new file mode 100644 index 0000000..1fd961b --- /dev/null +++ b/src/platforms/origin/mod.rs @@ -0,0 +1,6 @@ +mod origin_game; +mod origin_platform; +mod settings; + +pub use settings::OriginSettings; +pub(crate) use origin_platform::OriginPlatform; \ No newline at end of file diff --git a/src/origin/origin_game.rs b/src/platforms/origin/origin_game.rs similarity index 100% rename from src/origin/origin_game.rs rename to src/platforms/origin/origin_game.rs diff --git a/src/origin/origin_platform.rs b/src/platforms/origin/origin_platform.rs similarity index 81% rename from src/origin/origin_platform.rs rename to src/platforms/origin/origin_platform.rs index 191e1b4..795f18a 100644 --- a/src/origin/origin_platform.rs +++ b/src/platforms/origin/origin_platform.rs @@ -1,4 +1,4 @@ -use crate::platform::{Platform, SettingsValidity}; +use crate::platforms::{to_shortcuts, GamesPlatform, NeedsPorton, ShortcutToImport, FromSettingsString, load_settings}; use nom::bytes::complete::take_until; use std::{ fs::DirEntry, @@ -7,39 +7,37 @@ use std::{ use super::{origin_game::OriginGame, OriginSettings}; +#[derive(Clone)] pub struct OriginPlatform { pub settings: OriginSettings, } -impl Platform for OriginPlatform { - fn enabled(&self) -> bool { - self.settings.enabled - } - - fn name(&self) -> &str { - "Origin" - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { +impl NeedsPorton for OriginGame { + #[cfg(target_os = "windows")] + fn needs_proton(&self, _platform: &OriginPlatform) -> bool { false } - fn get_shortcuts(&self) -> Result, String> { + #[cfg(target_family = "unix")] + fn needs_proton(&self, _platform: &OriginPlatform) -> bool { + true + } + + fn create_symlinks(&self, _platform: &OriginPlatform) -> bool { + false + } +} + +impl OriginPlatform { + fn get_shortcuts(&self) -> eyre::Result> { let origin_folders = get_default_locations(); if origin_folders.is_none() { - return Err(String::from("Default path not found")); + return Err(eyre::format_err!("Default path not found")); } let origin_folders = origin_folders.unwrap(); let origin_folder = origin_folders.local_content_path; let origin_exe = origin_folders.exe_path; - let game_folders = origin_folder.join("LocalContent").read_dir().map_err(|e| { - format!( - "Could not read game dir: {} , error: {:?}", - origin_folder.join("LocalContent").to_string_lossy(), - e - ) - })?; + let game_folders = origin_folder.join("LocalContent").read_dir()?; let games = game_folders .filter_map(|folder| folder.ok()) .filter_map(|game_folder| { @@ -60,25 +58,6 @@ impl Platform for OriginPlatform { }); Ok(games.collect()) } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - let shortcuts_res = self.get_shortcuts(); - match shortcuts_res { - Ok(_) => SettingsValidity::Valid, - Err(err) => SettingsValidity::Invalid { - reason: err.to_string(), - }, - } - } - - fn needs_proton(&self, _input: &OriginGame) -> bool { - #[cfg(target_os = "windows")] - return false; - #[cfg(target_family = "unix")] - { - true - } - } } fn get_folder_mfst_file_content(game_folder_path: &Path) -> Option { @@ -200,3 +179,39 @@ fn get_exe_path() -> Option { } None } + +impl GamesPlatform for OriginPlatform { + fn name(&self) -> &str { + "Origin" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts(self, self.get_shortcuts()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Origin"); + ui.checkbox(&mut self.settings.enabled, "Import from Origin"); + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "origin" + } + + +} + + +impl FromSettingsString for OriginPlatform{ + fn from_settings_string>(s: S) -> Self { + OriginPlatform { settings: load_settings(s) } + } +} \ No newline at end of file diff --git a/src/platforms/origin/settings.rs b/src/platforms/origin/settings.rs new file mode 100644 index 0000000..824dd96 --- /dev/null +++ b/src/platforms/origin/settings.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] + +pub struct OriginSettings { + pub enabled: bool, +} + +impl Default for OriginSettings{ + fn default() -> Self { + Self { enabled: true } + } +} \ No newline at end of file diff --git a/src/platforms/platform.rs b/src/platforms/platform.rs new file mode 100644 index 0000000..baf39e0 --- /dev/null +++ b/src/platforms/platform.rs @@ -0,0 +1,82 @@ +use steam_shortcuts_util::shortcut::ShortcutOwned; +use dyn_clone::DynClone; + +pub trait GamesPlatform +where + Self: std::marker::Send, + Self: std::marker::Sync, + Self: DynClone, +{ + fn name(&self) -> &str; + + fn code_name(&self) -> &str; + + fn enabled(&self) -> bool; + + fn get_shortcut_info(&self) -> eyre::Result>; + + fn get_settings_serilizable(&self) -> String; + + fn render_ui(&mut self, ui: &mut egui::Ui); +} + +dyn_clone::clone_trait_object!(GamesPlatform); + +#[derive(Clone)] +pub struct ShortcutToImport { + pub shortcut: ShortcutOwned, + pub needs_proton: bool, + pub needs_symlinks: bool, +} + + +pub(crate) fn to_shortcuts( + platform: &P, + into_shortcuts: Result, eyre::ErrReport>, +) -> eyre::Result> +where + T: Into, + T: NeedsPorton

, +{ + let shortcuts = into_shortcuts?; + let mut shortcut_info = vec![]; + for m in shortcuts { + let needs_proton = m.needs_proton(platform); + let needs_symlinks = m.create_symlinks(platform); + let shortcut = m.into(); + shortcut_info.push(ShortcutToImport { + shortcut, + needs_proton, + needs_symlinks, + }); + } + Ok(shortcut_info) +} + +pub(crate) fn to_shortcuts_simple( + into_shortcuts: Result, eyre::ErrReport>, +) -> eyre::Result> +where + T: Into, +{ + let shortcuts = into_shortcuts?; + let mut shortcut_info = vec![]; + for m in shortcuts { + let needs_proton = false; + let needs_symlinks = false; + let shortcut = m.into(); + shortcut_info.push(ShortcutToImport { + shortcut, + needs_proton, + needs_symlinks, + }); + } + Ok(shortcut_info) +} + +pub trait NeedsPorton

{ + fn needs_proton(&self, platform: &P) -> bool; + + fn create_symlinks(&self, platform: &P) -> bool; +} + diff --git a/src/platforms/platforms_load.rs b/src/platforms/platforms_load.rs new file mode 100644 index 0000000..db493bb --- /dev/null +++ b/src/platforms/platforms_load.rs @@ -0,0 +1,115 @@ + +use std::collections::HashMap; + +use serde::de::DeserializeOwned; + +use crate::settings::load_setting_sections; + +use super::GamesPlatform; +use super::amazon::AmazonPlatform; +use super::bottles::BottlesPlatform; +use super::egs::EpicPlatform; +use super::flatpak::FlatpakPlatform; +use super::gog::GogPlatform; +use super::heroic::HeroicPlatform; +use super::itch::ItchPlatform; +use super::legendary::LegendaryPlatform; +use super::lutris::LutrisPlatform; +use super::origin::OriginPlatform; +use super::uplay::UplayPlatform; + +const PLATFORM_NAMES: [&str; 11] = [ + "amazon", + "bottles", + "epic_games", + "flatpak", + "gog", + "heroic", + "itch", + "legendary", + "lutris", + "origin", + "uplay", +]; + +pub type Platforms = Vec>; + +pub fn load_platform, B: AsRef>( + name: A, + settings_string: B, +) -> eyre::Result> { + let name = name.as_ref(); + let s = settings_string.as_ref(); + match name { + "amazon" => load::(s), + "bottles" => load::(s), + "epic_games" => load::(s), + "uplay" => load::(s), + "itch" => load::(s), + "flatpak" => load::(s), + "gog" => load::(s), + "heroic" => load::(s), + "legendary" => load::(s), + "lutris" => load::(s), + "origin" => load::(s), + _ => Err(eyre::format_err!("Unknown platform named {name}")), + } +} + + +pub fn get_platforms() -> Platforms { + let sections = load_setting_sections(); + let sections = match sections { + Ok(s) => s, + Err(err) => { + eprintln!( + "Could not load platform settings, using defaults: Error: {:?}", + err + ); + HashMap::new() + } + }; + + let mut platforms = vec![]; + for name in PLATFORM_NAMES { + let default =String::from(""); + let settings = sections.get(name).unwrap_or(&default); + match load_platform(name, settings) { + Ok(platform) => platforms.push(platform), + Err(e) => eprintln!("Could not load platform {name}, gave error: {e}"), + } + } + platforms +} + +pub fn load_settings>(input: S) -> Setting +where + Setting: Default, + Setting: DeserializeOwned, +{ + let str = input.as_ref(); + match toml::from_str(str) { + Ok(k) => k, + Err(err) => { + if !str.is_empty(){ + eprintln!("Error reading settings file {:?}", err); + } + Setting::default() + } + } +} + + +fn load(s: &str) -> eyre::Result> +where + T: FromSettingsString, + T: GamesPlatform, + T: 'static, +{ + Ok(Box::new(T::from_settings_string(s))) +} + + +pub trait FromSettingsString { + fn from_settings_string>(s: S) -> Self; +} diff --git a/src/uplay/game.rs b/src/platforms/uplay/game.rs similarity index 84% rename from src/uplay/game.rs rename to src/platforms/uplay/game.rs index b2b6243..663b19e 100644 --- a/src/uplay/game.rs +++ b/src/platforms/uplay/game.rs @@ -3,15 +3,15 @@ use std::path::{Path, PathBuf}; use steam_shortcuts_util::shortcut::{Shortcut, ShortcutOwned}; #[derive(Clone)] -pub(crate) struct Game { +pub(crate) struct UplayGame { pub(crate) name: String, pub(crate) icon: String, pub(crate) id: String, pub(crate) launcher: PathBuf, } -impl From for ShortcutOwned { - fn from(game: Game) -> Self { +impl From for ShortcutOwned { + fn from(game: UplayGame) -> Self { let launch = format!("\"uplay://launch/{}/0\"", game.id); let start_dir = game .launcher @@ -21,4 +21,4 @@ impl From for ShortcutOwned { let exe = format!("\"{}\"", game.launcher.to_string_lossy()); Shortcut::new("0", &game.name, &exe, &start_dir, &game.icon, "", &launch).to_owned() } -} +} \ No newline at end of file diff --git a/src/platforms/uplay/mod.rs b/src/platforms/uplay/mod.rs new file mode 100644 index 0000000..c947521 --- /dev/null +++ b/src/platforms/uplay/mod.rs @@ -0,0 +1,6 @@ +mod game; +mod platform; +mod settings; + +pub use platform::UplayPlatform; +pub use settings::UplaySettings; \ No newline at end of file diff --git a/src/platforms/uplay/platform.rs b/src/platforms/uplay/platform.rs new file mode 100644 index 0000000..759b9e1 --- /dev/null +++ b/src/platforms/uplay/platform.rs @@ -0,0 +1,128 @@ +#[cfg(target_os = "windows")] +use std::path::Path; +#[cfg(target_os = "windows")] +use std::path::PathBuf; + +use crate::platforms::FromSettingsString; +use crate::platforms::GamesPlatform; +use crate::platforms::load_settings; +use crate::platforms::to_shortcuts_simple; +use crate::platforms::ShortcutToImport; + +use super::{game::UplayGame, settings::UplaySettings}; + +#[derive(Clone)] +pub struct UplayPlatform { + pub settings: UplaySettings, +} + +fn get_uplay_games() -> eyre::Result> { + #[cfg(target_family = "unix")] + { + Err(eyre::format_err!("Uplay is not supported on Linux")) + } + #[cfg(target_os = "windows")] + { + get_games_from_winreg() + } +} + +#[cfg(target_os = "windows")] +fn get_launcher_path() -> eyre::Result { + use winreg::enums::*; + use winreg::RegKey; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let launcher_key = hklm.open_subkey("SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher")?; + let launcher_dir: String = launcher_key.get_value("InstallDir")?; + let path = Path::new(&launcher_dir).join("upc.exe"); + if path.exists() { + Ok(path) + } else { + Err(eyre::eyre!( + "Could not find uplay launcher at path {:?}", + path + )) + } +} + +#[cfg(target_os = "windows")] +fn get_games_from_winreg() -> eyre::Result> { + use winreg::enums::*; + use winreg::RegKey; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let mut games = vec![]; + let mut installed_ids = vec![]; + let launcher_path = get_launcher_path()?; + + if let Ok(installs) = hklm.open_subkey("SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher\\Installs") { + for i in installs.enum_keys().filter_map(|i| i.ok()) { + if let Ok(install) = installs.open_subkey(&i) { + let install_dir: Result = install.get_value("InstallDir"); + if let Ok(folder) = install_dir { + let path = Path::new(&folder); + if path.exists() { + installed_ids.push(i); + } + } + } + } + } + + for id in installed_ids { + let path = format!("SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Uplay Install {}",id); + let subkey = hklm.open_subkey(path); + if let Ok(subkey) = subkey { + let name: Result = subkey.get_value("DisplayName"); + if let Ok(name) = name { + let icon: String = subkey.get_value("DisplayIcon").unwrap_or_default(); + games.push(UplayGame { + name, + icon, + id, + launcher: launcher_path.clone(), + }) + } + } + } + + Ok(games) +} + +impl FromSettingsString for UplayPlatform { + fn from_settings_string>(s: S) -> Self { + UplayPlatform { + settings: load_settings(s), + } + } +} + + +impl GamesPlatform for UplayPlatform{ + fn name(&self) -> &str { + "Uplay" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(get_uplay_games()) + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Uplay"); + ui.checkbox(&mut self.settings.enabled, "Import from Uplay"); + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn code_name(&self) -> &str { + "uplay" + } + +} \ No newline at end of file diff --git a/src/platforms/uplay/settings.rs b/src/platforms/uplay/settings.rs new file mode 100644 index 0000000..1a67466 --- /dev/null +++ b/src/platforms/uplay/settings.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct UplaySettings { + pub enabled: bool, +} + +impl Default for UplaySettings { + fn default() -> Self { + #[cfg(target_family = "unix")] + let enabled = false; + #[cfg(target_family = "windows")] + let enabled = true; + Self { enabled } + } +} diff --git a/src/settings.rs b/src/settings.rs index 9fbfb03..fcf6c8f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,40 +1,19 @@ use crate::{ - amazon::AmazonSettings, config::get_config_file, egs::EpicGamesLauncherSettings, - flatpak::FlatpakSettings, gog::GogSettings, itch::ItchSettings, legendary::LegendarySettings, - lutris::settings::LutrisSettings, origin::OriginSettings, steam::SteamSettings, - steamgriddb::SteamGridDbSettings, uplay::UplaySettings, + config::get_config_file, platforms::Platforms, steam::SteamSettings, + steamgriddb::SteamGridDbSettings, }; -#[cfg(target_family = "unix")] -use crate::heroic::HeroicSettings; - -#[cfg(target_family = "unix")] -use crate::bottles::BottlesSettings; - use config::{Config, ConfigError, Environment, File}; use serde::{Deserialize, Serialize}; -use std::env; +use std::{collections::HashMap, env}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Settings { pub debug: bool, pub config_version: Option, pub blacklisted_games: Vec, - pub epic_games: EpicGamesLauncherSettings, - pub legendary: LegendarySettings, - pub itch: ItchSettings, pub steamgrid_db: SteamGridDbSettings, pub steam: SteamSettings, - pub origin: OriginSettings, - pub gog: GogSettings, - pub uplay: UplaySettings, - pub lutris: LutrisSettings, - #[cfg(target_family = "unix")] - pub heroic: HeroicSettings, - pub amazon: AmazonSettings, - pub flatpak: FlatpakSettings, - #[cfg(target_family = "unix")] - pub bottles: BottlesSettings, } impl Settings { @@ -65,10 +44,66 @@ impl Settings { } } +pub fn load_setting_sections() -> eyre::Result> { + let config_file_path = get_config_file(); + let content = std::fs::read_to_string(config_file_path)?; + let mut result = HashMap::new(); + let lines = content.lines(); + let mut current_section_lines: Vec = vec![]; + let mut current_section_name: Option = Option::None; + for line in lines { + if line.starts_with('[') && line.ends_with(']') { + add_sections(¤t_section_name, ¤t_section_lines, &mut result); + current_section_name = Some(line[1..line.len() - 1].to_string()); + current_section_lines.clear(); + } else { + current_section_lines.push(line.to_string()); + } + } + add_sections(¤t_section_name, ¤t_section_lines, &mut result); + + let blacklisted_sections = ["steamgrid_db", "steam"]; + for section in blacklisted_sections { + let _ = result.remove(section); + } + Ok(result) +} + +pub fn save_settings(settings: &Settings, platforms: &Platforms) { + let mut toml = toml::to_string(&settings).unwrap(); + + for platform in platforms { + let section_name = format!("[{}]", platform.code_name()); + toml.push('\n'); + toml.push_str(section_name.as_str()); + toml.push('\n'); + let platform_string = platform.get_settings_serilizable(); + toml.push_str(platform_string.as_str()); + } + + let config_path = crate::config::get_config_file(); + std::fs::write(config_path, toml).unwrap(); +} + +fn add_sections( + current_section_name: &Option, + current_section_lines: &Vec, + result: &mut HashMap, +) { + if let Some(old_section_name) = current_section_name { + let mut section_string = String::new(); + for line in current_section_lines { + section_string.push_str(line); + section_string.push('\n'); + } + result.insert(old_section_name.to_string(), section_string); + } +} + fn sanitize_auth_key(result: &mut Settings) { if let Some(auth_key) = result.steamgrid_db.auth_key.as_ref() { if auth_key == "Write your authentication key between these quotes" { result.steamgrid_db.auth_key = None; } } -} +} \ No newline at end of file diff --git a/src/steam/mod.rs b/src/steam/mod.rs index 58730e4..67940f6 100644 --- a/src/steam/mod.rs +++ b/src/steam/mod.rs @@ -1,5 +1,6 @@ mod collections; mod installed_games; +#[cfg(target_family = "unix")] mod proton_vdf_util; mod restarter; mod settings; @@ -7,6 +8,7 @@ mod utils; pub use collections::*; pub use installed_games::*; +#[cfg(target_family = "unix")] pub use proton_vdf_util::*; pub use restarter::*; pub use settings::SteamSettings; diff --git a/src/steam/utils.rs b/src/steam/utils.rs index 3ca667a..cbf4827 100644 --- a/src/steam/utils.rs +++ b/src/steam/utils.rs @@ -43,7 +43,7 @@ pub struct ShortcutInfo { pub shortcuts: Vec, } -#[derive(Default, PartialEq, Clone, Debug)] +#[derive(Default, PartialEq, Eq, Clone, Debug)] pub struct SteamUsersInfo { pub steam_user_data_folder: String, pub shortcut_path: Option, diff --git a/src/steamgriddb/image_type.rs b/src/steamgriddb/image_type.rs index 784fe19..cc34736 100644 --- a/src/steamgriddb/image_type.rs +++ b/src/steamgriddb/image_type.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImageType { Hero, Grid, diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 4b4ca83..8a2446a 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -1,11 +1,11 @@ #[cfg(target_family = "unix")] -mod symlinks; +pub mod symlinks; mod synchronization; pub use synchronization::download_images; -pub use synchronization::get_platform_shortcuts; -pub use synchronization::run_sync; pub use synchronization::IsBoilRShortcut; pub use synchronization::SyncProgress; pub use synchronization::*; + + diff --git a/src/sync/synchronization.rs b/src/sync/synchronization.rs index b0cd81e..55b2604 100644 --- a/src/sync/synchronization.rs +++ b/src/sync/synchronization.rs @@ -1,29 +1,20 @@ -use steam_shortcuts_util::{shortcut::ShortcutOwned, shortcuts_to_bytes, calculate_app_id_for_shortcut, Shortcut}; +use steam_shortcuts_util::{ + calculate_app_id_for_shortcut, shortcut::ShortcutOwned, shortcuts_to_bytes, Shortcut, +}; use tokio::sync::watch::Sender; use crate::{ - egs::EpicPlatform, - legendary::LegendaryPlatform, - lutris::lutris_platform::LutrisPlatform, - platform::Platform, + platforms::{GamesPlatform, ShortcutToImport}, settings::Settings, steam::{ - get_shortcuts_for_user, get_shortcuts_paths, setup_proton_games, write_collections, + get_shortcuts_for_user, get_shortcuts_paths, write_collections, Collection, ShortcutInfo, SteamUsersInfo, }, steamgriddb::{download_images_for_users, ImageType}, - uplay::Uplay, }; -#[cfg(target_family = "unix")] -use crate::heroic::HeroicPlatform; +use std::{collections::HashMap, error::Error}; -#[cfg(target_family = "unix")] -use crate::flatpak::FlatpakPlatform; - -use std::{error::Error, collections::HashMap}; - -use crate::{gog::GogPlatform, itch::ItchPlatform, origin::OriginPlatform}; use std::{fs::File, io::Write, path::Path}; pub const BOILR_TAG: &str = "boilr"; @@ -56,30 +47,29 @@ pub fn disconnect_shortcut(settings: &Settings, app_id: u32) -> Result<(), Strin Ok(()) } -pub fn run_sync( +pub fn sync_shortcuts( settings: &Settings, + platform_shortcuts: &[(String, Vec)], sender: &mut Option>, - renames: &HashMap + renames: &HashMap, ) -> Result, String> { - if let Some(sender) = &sender { - let _ = sender.send(SyncProgress::Starting); - } let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam) .map_err(|e| format!("Getting shortcut paths failed: {e}"))?; - - let platform_shortcuts = get_platform_shortcuts(settings); let mut all_shortcuts: Vec = platform_shortcuts .iter() .flat_map(|s| s.1.clone()) .filter(|s| !settings.blacklisted_games.contains(&s.app_id)) .collect(); + for shortcut in &mut all_shortcuts{ + shortcut.dev_kit_game_id = BOILR_TAG.to_string(); + } if let Some(sender) = &sender { let _ = sender.send(SyncProgress::FoundGames { games_found: all_shortcuts.len(), }); } for shortcut in &mut all_shortcuts { - if let Some(rename) = renames.get(&shortcut.app_id){ + if let Some(rename) = renames.get(&shortcut.app_id) { shortcut.app_name = rename.clone(); let new_shortcut = Shortcut::new( "0", @@ -88,7 +78,7 @@ pub fn run_sync( "", "", "", - "" + "", ); shortcut.app_id = calculate_app_id_for_shortcut(&new_shortcut); } @@ -118,7 +108,7 @@ pub fn run_sync( save_shortcuts(&shortcut_info.shortcuts, Path::new(&shortcut_info.path)); if settings.steam.create_collections { - match write_shortcut_collections(&user.user_id, &platform_shortcuts) { + match write_shortcut_collections(&user.user_id, platform_shortcuts) { Ok(_) => (), Err(_e) => eprintln!("Could not write collections, make sure steam is shut down"), } @@ -127,7 +117,6 @@ pub fn run_sync( let duration = start_time.elapsed(); println!("Finished synchronizing games in: {:?}", duration); } - Ok(userinfo_shortcuts) } @@ -211,47 +200,14 @@ fn write_shortcut_collections>( Ok(()) } -pub fn get_platform_shortcuts(settings: &Settings) -> Vec<(String, Vec)> { - let mut platform_results = vec![ - update_platform_shortcuts(&EpicPlatform::new(&settings.epic_games)), - update_platform_shortcuts(&LegendaryPlatform::new(settings.legendary.clone())), - update_platform_shortcuts(&ItchPlatform::new(settings.itch.clone())), - update_platform_shortcuts(&OriginPlatform { - settings: settings.origin.clone(), - }), - update_platform_shortcuts(&GogPlatform { - settings: settings.gog.clone(), - }), - update_platform_shortcuts(&Uplay { - settings: settings.uplay.clone(), - }), - update_platform_shortcuts(&LutrisPlatform { - settings: settings.lutris.clone(), - }), - ]; - #[cfg(target_family = "unix")] - { - platform_results.push(update_platform_shortcuts(&HeroicPlatform { - settings: settings.heroic.clone(), - })); - - platform_results.push(update_platform_shortcuts(&FlatpakPlatform { - settings: settings.flatpak.clone(), - })); - platform_results.push(update_platform_shortcuts(&crate::bottles::BottlesPlatform { - settings: settings.bottles.clone(), - })); - - +pub fn get_platform_shortcuts( + platform: Box, +) -> eyre::Result> { + if platform.enabled() { + platform.get_shortcut_info() + }else{ + Ok(vec![]) } - - #[cfg(windows)] - { - platform_results.push(update_platform_shortcuts(&crate::amazon::AmazonPlatform { - settings: settings.amazon.clone(), - })); - } - platform_results.iter().filter_map(|p| p.clone()).collect() } fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) { @@ -280,77 +236,3 @@ fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) { } } } - -fn update_platform_shortcuts(platform: &P) -> Option<(String, Vec)> -where - P: Platform, - E: std::fmt::Debug + std::fmt::Display, - T: Into, - T: Clone, -{ - if platform.enabled() { - if let crate::platform::SettingsValidity::Invalid { reason } = platform.settings_valid() { - eprintln!( - "Setting for platform {} are invalid, reason: {}", - platform.name(), - reason - ); - return None; - } - - let mut current_shortcuts = vec![]; - - #[cfg(target_family = "unix")] - if platform.create_symlinks() { - let name = platform.name(); - super::symlinks::ensure_links_folder_created(name); - } - - let shortcuts_to_add_result = platform.get_shortcuts(); - - match shortcuts_to_add_result { - Ok(shortcuts_to_add) => { - let mut shortcuts_to_proton = vec![]; - let mut shortcuts_to_add_transformed = vec![]; - for shortcut in shortcuts_to_add { - let mut shortcut_owned: ShortcutOwned = shortcut.clone().into(); - shortcut_owned.dev_kit_game_id = - format!("{}-{}", BOILR_TAG, shortcut_owned.app_id); - shortcuts_to_add_transformed.push((shortcut, shortcut_owned)); - } - - let shortcuts_to_add = shortcuts_to_add_transformed; - - println!( - "Found {} game(s) for platform {}", - shortcuts_to_add.len(), - platform.name() - ); - - for (orign_shortcut, shortcut_owned) in shortcuts_to_add { - #[cfg(target_family = "unix")] - let shortcut_owned = if platform.create_symlinks() { - crate::sync::symlinks::create_sym_links(&shortcut_owned) - } else { - shortcut_owned - }; - if platform.needs_proton(&orign_shortcut) { - shortcuts_to_proton.push(format!("{}", shortcut_owned.app_id)); - } - current_shortcuts.push(shortcut_owned.clone()); - } - if !shortcuts_to_proton.is_empty() { - setup_proton_games(shortcuts_to_proton.as_slice()); - } - - let name = platform.name(); - return Some((name.to_string(), current_shortcuts)); - } - Err(err) => { - eprintln!("Error getting shortcuts from platform: {}", platform.name()); - eprintln!("{}", err); - } - } - } - None -} diff --git a/src/ui/defines.rs b/src/ui/defines.rs index 434f7ca..36c8e6f 100644 --- a/src/ui/defines.rs +++ b/src/ui/defines.rs @@ -43,8 +43,12 @@ pub mod ui_images { } } - pub fn load_image_from_path(path: &std::path::Path) -> Result { - let image = image::io::Reader::open(path)?.with_guessed_format()?.decode()?; + pub fn load_image_from_path( + path: &std::path::Path, + ) -> Result { + let image = image::io::Reader::open(path)? + .with_guessed_format()? + .decode()?; let size = [image.width() as _, image.height() as _]; let image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); @@ -53,18 +57,14 @@ pub mod ui_images { pixels.as_slice(), )) } - - pub fn load_image_from_memory(image_data: &[u8]) -> Result { + + pub fn load_image_from_memory(image_data: &[u8]) -> Result { let image = image::load_from_memory(image_data)?; let size = [image.width() as _, image.height() as _]; let image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); - Ok(ColorImage::from_rgba_unmultiplied( - size, - pixels.as_slice(), - )) + Ok(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())) } - } #[cfg(test)] @@ -95,9 +95,9 @@ mod tests { assert!(res.is_err()); } - #[test] - pub fn test_image_load_animated_webp2() { - let res = load_image_from_path(std::path::Path::new("src/testdata/tunic.webp")); - assert!(res.is_err()); - } + // #[test] + // pub fn test_image_load_animated_webp2() { + // let res = load_image_from_path(std::path::Path::new("src/testdata/tunic.webp")); + // assert!(res.is_err()); + // } } diff --git a/src/ui/ui_disconnect.rs b/src/ui/ui_disconnect.rs index 3b5548c..4f9610d 100644 --- a/src/ui/ui_disconnect.rs +++ b/src/ui/ui_disconnect.rs @@ -57,13 +57,8 @@ impl MyEguiApp { ui.heading(&user.path.to_string_lossy().to_string()); } for shortcut in user.shortcuts.iter() { - if shortcut.is_boilr_shortcut() - && ui.button(&shortcut.app_name).clicked() - { - if disconnect_shortcut(&self.settings, shortcut.app_id).is_ok() - { - redraw = shortcut.app_id; - } + if shortcut.is_boilr_shortcut() && ui.button(&shortcut.app_name).clicked() && disconnect_shortcut(&self.settings, shortcut.app_id).is_ok() { + redraw = shortcut.app_id; } } } diff --git a/src/ui/ui_image_download.rs b/src/ui/ui_image_download.rs index 1fece8b..4b05293 100644 --- a/src/ui/ui_image_download.rs +++ b/src/ui/ui_image_download.rs @@ -353,7 +353,6 @@ impl MyEguiApp { } pub(crate) fn render_ui_images(&mut self, ui: &mut egui::Ui) { - self.ensure_games_loaded(); self.ensure_steam_users_loaded(); if let Some(error_message) = &self.image_selected_state.settings_error { diff --git a/src/ui/ui_import_games.rs b/src/ui/ui_import_games.rs index 76f38b1..bf1f818 100644 --- a/src/ui/ui_import_games.rs +++ b/src/ui/ui_import_games.rs @@ -2,15 +2,18 @@ use eframe::egui; use egui::ScrollArea; use futures::executor::block_on; +use steam_shortcuts_util::shortcut::ShortcutOwned; use tokio::sync::watch; use crate::config::get_renames_file; -use crate::settings::Settings; +use crate::platforms::ShortcutToImport; +#[cfg(target_family = "unix")] +use crate::steam::setup_proton_games; use crate::sync; use crate::sync::{download_images, SyncProgress}; -use super::{backup_shortcuts, ImageSelectState}; +use super::{backup_shortcuts, all_ready, get_all_games}; use super::{ ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, MyEguiApp, @@ -32,22 +35,12 @@ impl FetcStatus { FetcStatus::Fetched(_) => true, } } - - pub fn needs_fetching(&self) -> bool { - match self { - FetcStatus::NeedsFetched => true, - FetcStatus::Fetching => false, - FetcStatus::Fetched(_) => false, - } - } } impl MyEguiApp { pub(crate) fn render_import_games(&mut self, ui: &mut egui::Ui) { ui.heading("Import Games"); - self.ensure_games_loaded(); - let mut scroll_style = ui.style_mut(); scroll_style.visuals.extreme_bg_color = BACKGROUND_COLOR; scroll_style.visuals.widgets.inactive.bg_fill = EXTRA_BACKGROUND_COLOR; @@ -60,83 +53,79 @@ impl MyEguiApp { .auto_shrink([false,true]) .show(ui,|ui| { ui.reset_style(); - - let borrowed_games = &*self.games_to_sync.borrow(); - match borrowed_games{ - FetcStatus::Fetched(games_to_sync) => { - ui.label("Select the games you want to import into steam"); - for (platform_name, shortcuts) in games_to_sync{ - ui.heading(platform_name); - - for shortcut in shortcuts { - let mut import_game = !self.settings.blacklisted_games.contains(&shortcut.app_id); - ui.horizontal(|ui|{ - if self.current_edit == Option::Some(shortcut.app_id){ - if let Some(new_name) = self.rename_map.get_mut(&shortcut.app_id){ - ui.text_edit_singleline(new_name).request_focus(); - if ui.button("Rename").clicked() { - if new_name.is_empty(){ - *new_name = shortcut.app_name.to_string(); + ui.label("Select the games you want to import into steam"); + for (name,status) in &self.games_to_sync{ + ui.heading(name); + match &*status.borrow(){ + FetcStatus::NeedsFetched => {ui.label("Need to find games");}, + FetcStatus::Fetching => { + ui.horizontal(|ui|{ + ui.spinner(); + ui.label("Finding installed games"); + }); + }, + FetcStatus::Fetched(shortcuts) => { + match shortcuts{ + Ok(shortcuts) => { + if shortcuts.is_empty(){ + ui.label("Did not find any games"); + } + for shortcut_to_import in shortcuts { + let shortcut = &shortcut_to_import.shortcut; + let mut import_game = !self.settings.blacklisted_games.contains(&shortcut.app_id); + ui.horizontal(|ui|{ + if self.current_edit == Option::Some(shortcut.app_id){ + if let Some(new_name) = self.rename_map.get_mut(&shortcut.app_id){ + ui.text_edit_singleline(new_name).request_focus(); + if ui.button("Rename").clicked() { + if new_name.is_empty(){ + *new_name = shortcut.app_name.to_string(); + } + self.current_edit = Option::None; + let rename_file_path = get_renames_file(); + let contents = serde_json::to_string(&self.rename_map); + if let Ok(contents) = contents{ + let res = std::fs::write(&rename_file_path, contents); + println!("Write rename file at {:?} with result: {:?}",rename_file_path, res); + } + } } - self.current_edit = Option::None; - let rename_file_path = get_renames_file(); - let contents = serde_json::to_string(&self.rename_map); - if let Ok(contents) = contents{ - let res = std::fs::write(&rename_file_path, contents); - println!("Write rename file at {:?} with result: {:?}",rename_file_path, res); + } else { + let name = self.rename_map.get(&shortcut.app_id).unwrap_or(&shortcut.app_name); + let checkbox = egui::Checkbox::new(&mut import_game,name); + let response = ui.add(checkbox); + if response.double_clicked(){ + self.rename_map.entry(shortcut.app_id).or_insert_with(|| shortcut.app_name.to_owned()); + self.current_edit = Option::Some(shortcut.app_id); + } + if response.clicked(){ + if !self.settings.blacklisted_games.contains(&shortcut.app_id){ + self.settings.blacklisted_games.push(shortcut.app_id); + }else{ + self.settings.blacklisted_games.retain(|id| *id != shortcut.app_id); } } - } - } else { - let name = self.rename_map.get(&shortcut.app_id).unwrap_or(&shortcut.app_name); - let checkbox = egui::Checkbox::new(&mut import_game,name); - let response = ui.add(checkbox); - if response.double_clicked(){ - if !self.rename_map.contains_key(&shortcut.app_id){ - self.rename_map.insert(shortcut.app_id,shortcut.app_name.to_owned()); - } - self.current_edit = Option::Some(shortcut.app_id); + } + + }); } - if response.clicked(){ - if !self.settings.blacklisted_games.contains(&shortcut.app_id){ - self.settings.blacklisted_games.push(shortcut.app_id); - }else{ - self.settings.blacklisted_games.retain(|id| *id != shortcut.app_id); - } - } - } - - }); - } - } - ui.add_space(SECTION_SPACING); - ui.label("Check the settings if BoilR didn't find the game you where looking for"); - }, - _=> { - ui.ctx().request_repaint(); - ui.horizontal(|ui|{ - ui.spinner(); - ui.label("Finding installed games"); - }); - }, + }, + Err(err) => { + ui.label("Failed finding games").on_hover_text(format!("Error message: {err}")); + }, + }; + + }, + } + }; + ui.add_space(SECTION_SPACING); + + ui.label("Check the settings if BoilR didn't find the game you where looking for"); }); } - pub fn ensure_games_loaded(&mut self) { - if self.games_to_sync.borrow().needs_fetching() { - self.image_selected_state = ImageSelectState::default(); - let (tx, rx) = watch::channel(FetcStatus::NeedsFetched); - self.games_to_sync = rx; - let settings = self.settings.clone(); - self.rt.spawn_blocking(move || { - let _ = tx.send(FetcStatus::Fetching); - let games_to_sync = sync::get_platform_shortcuts(&settings); - let _ = tx.send(FetcStatus::Fetched(games_to_sync)); - }); - } - } - + pub fn run_sync(&mut self, wait: bool ) { let (sender, reciever) = watch::channel(SyncProgress::NotStarted); let settings = self.settings.clone(); @@ -144,34 +133,77 @@ impl MyEguiApp { crate::steam::ensure_steam_stopped(); } + //TODO This might break cli sync, test it + self.status_reciever = reciever; let renames = self.rename_map.clone(); - let handle = self.rt.spawn_blocking(move || { - MyEguiApp::save_settings_to_file(&settings); - let mut some_sender = Some(sender); - backup_shortcuts(&settings.steam); - let usersinfo = sync::run_sync(&settings, &mut some_sender,&renames).unwrap(); - let task = download_images(&settings, &usersinfo, &mut some_sender); - block_on(task); + let all_ready= all_ready(&self.games_to_sync); + let _ = sender.send(SyncProgress::Starting); + if all_ready{ + let shortcuts_to_import = get_all_games(&self.games_to_sync); + let handle = self.rt.spawn_blocking(move || { - //Run a second time to fix up shortcuts after images are downloaded - sync::run_sync(&settings, &mut some_sender,&renames).unwrap(); + #[cfg(target_family = "unix")] + setup_proton(shortcuts_to_import.iter()); - if let Some(sender) = some_sender { - let _ = sender.send(SyncProgress::Done); + let import_games = to_shortcut_owned(shortcuts_to_import); + + let mut some_sender = Some(sender); + backup_shortcuts(&settings.steam); + let usersinfo = sync::sync_shortcuts(&settings, &import_games, &mut some_sender,&renames).unwrap(); + let task = download_images(&settings, &usersinfo, &mut some_sender); + block_on(task); + //Run a second time to fix up shortcuts after images are downloaded + sync::sync_shortcuts(&settings, &import_games, &mut some_sender,&renames).unwrap(); + + if let Some(sender) = some_sender { + let _ = sender.send(SyncProgress::Done); + } + if settings.steam.start_steam { + crate::steam::ensure_steam_started(&settings.steam); + } + }); + if wait { + self.rt.block_on(handle).unwrap(); } - if settings.steam.start_steam { - crate::steam::ensure_steam_started(&settings.steam); - } - }); - if wait { - self.rt.block_on(handle).unwrap(); } } - pub fn save_settings_to_file(settings: &Settings) { - let toml = toml::to_string(&settings).unwrap(); - let config_path = crate::config::get_config_file(); - std::fs::write(config_path, toml).unwrap(); - } + } + +fn to_shortcut_owned(shortcuts_to_import: Vec<(String, Vec)>) -> Vec<(String, Vec)> { + let mut import_games = vec![]; + for(name,infos) in shortcuts_to_import{ + let mut shortcuts = vec![]; + for info in infos{ + shortcuts.push(info.shortcut); + } + import_games.push((name,shortcuts)); + } + import_games +} + + + #[cfg(target_family = "unix")] + fn setup_proton<'a, I>(shortcut_infos: I) + where I: IntoIterator)> + { + let mut shortcuts_to_proton = vec![]; + + for (name,shortcuts) in shortcut_infos { + for shortcut_info in shortcuts{ + if shortcut_info.needs_proton { + crate::sync::symlinks::ensure_links_folder_created(&name); + } + if shortcut_info.needs_proton { + shortcuts_to_proton.push(format!("{}", shortcut_info.shortcut.app_id)); + } + + if shortcut_info.needs_symlinks { + crate::sync::symlinks::create_sym_links(&shortcut_info.shortcut); + } + } + setup_proton_games(&shortcuts_to_proton); + } +} \ No newline at end of file diff --git a/src/ui/ui_settings.rs b/src/ui/ui_settings.rs index d665c23..74cc2f7 100644 --- a/src/ui/ui_settings.rs +++ b/src/ui/ui_settings.rs @@ -2,10 +2,6 @@ use copypasta::ClipboardProvider; use eframe::egui; use egui::ScrollArea; -use crate::{egs::EpicPlatform}; -#[cfg(target_family = "unix")] -use crate::heroic::HeroicPlatform; - use super::{ ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, MyEguiApp, @@ -34,206 +30,15 @@ impl MyEguiApp { self.render_steam_settings(ui); - self.render_epic_settings(ui); - - #[cfg(target_family = "unix")] - { - self.render_heroic_settings(ui); - self.render_bottles_settings(ui); + for platform in &mut self.platforms{ + platform.render_ui(ui); + ui.add_space(SECTION_SPACING); } - - self.render_legendary_settings(ui); - self.render_itch_settings(ui); - self.render_origin_settings(ui); - self.render_gog_settings(ui); - self.render_uplay_settings(ui); - self.render_lutris_settings(ui); - #[cfg(windows)] - { - self.render_amazon_settings(ui); - } - - - #[cfg(target_family = "unix")] - { - self.render_flatpak_settings(ui); - } - - - ui.add_space(SECTION_SPACING); ui.label(format!("Version: {}", VERSION)); }); } - #[cfg(target_family = "unix")] - fn render_flatpak_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Flatpak"); - ui.checkbox(&mut self.settings.flatpak.enabled, "Import from Flatpak"); - - ui.add_space(SECTION_SPACING); - } - - - #[cfg(target_family = "unix")] - fn render_bottles_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Bottles"); - ui.checkbox(&mut self.settings.bottles.enabled, "Import from Bottles"); - ui.add_space(SECTION_SPACING); - } - - -#[cfg(target_family = "unix")] - fn render_heroic_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Heroic"); - ui.checkbox(&mut self.settings.heroic.enabled, "Import from Heroic"); - ui.checkbox(&mut self.settings.heroic.default_launch_through_heroic, "Always launch games through Heroic"); - let safe_mode_header = match (self.settings.heroic.default_launch_through_heroic,self.settings.heroic.launch_games_through_heroic.len()) { - (false,0) => "Force games to launch through Heroic Launcher".to_string(), - (false,1) => "One game forced to launch through Heroic Launcher".to_string(), - (false,x) => format!("{} games forced to launch through Heroic Launcher", x), - - (true,0) => "Force games to launch directly".to_string(), - (true,1) => "One game forced to launch directly".to_string(), - (true,x) => format!("{} games forced to launch directly", x), - - }; - egui::CollapsingHeader::new(safe_mode_header) - .id_source("Heroic_Launcher_safe_launch") - .show(ui, |ui| { -if -self.settings.heroic.default_launch_through_heroic{ - ui.label("Some games work best when launched directly, select those games below and BoilR will create shortcuts that launch the games directly."); - - } else{ - ui.label("Some games must be started from the Heroic Launcher, select those games below and BoilR will create shortcuts that opens the games through the Heroic Launcher."); - - } - #[cfg(target_family = "unix")]{ - let manifests =self.heroic_games.get_or_insert_with(||{ - let heroic_setting = self.settings.heroic.clone(); - - let heroic_platform =HeroicPlatform{ - settings:heroic_setting - }; - heroic_platform.get_heroic_games() - }); - - let safe_open_games = &mut self.settings.heroic.launch_games_through_heroic; - for manifest in manifests{ - let key = manifest.app_name(); - let display_name = manifest.title(); - let mut safe_open = safe_open_games.contains(&display_name.to_string()) || safe_open_games.contains(&key.to_string()); - if ui.checkbox(&mut safe_open, display_name).clicked(){ - if safe_open{ - safe_open_games.push(key.to_string()); - }else{ - safe_open_games.retain(|m| m!= display_name && m!= key); - } - } - } - } - }) ; - ui.add_space(SECTION_SPACING); - } - - fn render_lutris_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Lutris"); - ui.checkbox(&mut self.settings.lutris.enabled, "Import from Lutris"); - if self.settings.lutris.enabled { - ui.checkbox(&mut self.settings.lutris.flatpak, "Flatpak version"); - if !self.settings.lutris.flatpak { - ui.horizontal(|ui| { - let lutris_location = &mut self.settings.lutris.executable; - ui.label("Lutris Location: "); - ui.text_edit_singleline(lutris_location); - }); - } else { - ui.horizontal(|ui| { - let flatpak_image = &mut self.settings.lutris.flatpak_image; - ui.label("Flatpak image"); - ui.text_edit_singleline(flatpak_image); - }); - } - } - ui.add_space(SECTION_SPACING); - } - - #[cfg(windows)] - fn render_amazon_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Amazon"); - ui.checkbox(&mut self.settings.amazon.enabled, "Import from Amazon"); - ui.add_space(SECTION_SPACING); - } - - fn render_uplay_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Uplay"); - ui.checkbox(&mut self.settings.uplay.enabled, "Import from Uplay"); - ui.add_space(SECTION_SPACING); - } - - fn render_gog_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("GoG Galaxy"); - ui.checkbox(&mut self.settings.gog.enabled, "Import from GoG Galaxy"); - if self.settings.gog.enabled { - ui.horizontal(|ui| { - let mut empty_string = "".to_string(); - let itch_location = self - .settings - .gog - .location - .as_mut() - .unwrap_or(&mut empty_string); - ui.label("GoG Galaxy Folder: "); - if ui.text_edit_singleline(itch_location).changed() { - self.settings.gog.location = Some(itch_location.to_string()); - } - }); - } - ui.add_space(SECTION_SPACING); - } - - fn render_origin_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Origin"); - ui.checkbox(&mut self.settings.origin.enabled, "Import from Origin"); - ui.add_space(SECTION_SPACING); - } - - fn render_itch_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Itch.io"); - ui.checkbox(&mut self.settings.itch.enabled, "Import from Itch.io"); - if self.settings.itch.enabled { - ui.horizontal(|ui| { - let mut empty_string = "".to_string(); - let itch_location = self - .settings - .itch - .location - .as_mut() - .unwrap_or(&mut empty_string); - ui.label("Itch.io Folder: "); - if ui.text_edit_singleline(itch_location).changed() { - self.settings.itch.location = if itch_location.is_empty(){ - None - }else { - Some(itch_location.to_string()) - }; - }else{ - if !itch_location.is_empty(){ - if ui.button("Reset").on_hover_text("Reset the itch path, let BoilR guess again").clicked(){ - self.settings.itch.location = None; - } - } - } - - }); - #[cfg(target_family = "unix")] - { - ui.checkbox(&mut self.settings.itch.create_symlinks, "Create symlinks"); - } - } - ui.add_space(SECTION_SPACING); - } - + fn render_steam_settings(&mut self, ui: &mut egui::Ui) { ui.heading("Steam"); ui.horizontal(|ui| { @@ -317,68 +122,4 @@ self.settings.heroic.default_launch_through_heroic{ } ui.add_space(SECTION_SPACING); } - - fn render_legendary_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Legendary & Rare"); - ui.checkbox( - &mut self.settings.legendary.enabled, - "Import from Legendary & Rare", - ); - if self.settings.legendary.enabled { - ui.horizontal(|ui| { - let mut empty_string = "".to_string(); - let legendary_location = self - .settings - .legendary - .executable - .as_mut() - .unwrap_or(&mut empty_string); - ui.label("Legendary Executable: ") - .on_hover_text("The location of the legendary executable to use"); - if ui.text_edit_singleline(legendary_location).changed() { - self.settings.legendary.executable = Some(legendary_location.to_string()); - } - }); - } - ui.add_space(SECTION_SPACING); - } - - fn render_epic_settings(&mut self, ui: &mut egui::Ui) { - let epic_settings = &mut self.settings.epic_games; - ui.heading("Epic Games"); - ui.checkbox(&mut epic_settings.enabled, "Import from Epic Games"); - if epic_settings.enabled { - let safe_mode_header = match epic_settings.safe_launch.len() { - 0 => "Force games to launch through Epic Launcher".to_string(), - 1 => "One game forced to launch through Epic Launcher".to_string(), - x => format!("{} games forced to launch through Epic Launcher", x), - }; - - egui::CollapsingHeader::new(safe_mode_header) - .id_source("Epic_Launcher_safe_launch") - .show(ui, |ui| { - ui.label("Some games must be started from the Epic Launcher, select those games below and BoilR will create shortcuts that opens the games through the Epic Launcher."); - let manifests =self.epic_manifests.get_or_insert_with(||{ - let epic_platform = EpicPlatform::new(epic_settings); - let manifests = crate::platform::Platform::get_shortcuts(&epic_platform); - manifests.unwrap_or_default() - }); - let mut safe_open_games = epic_settings.safe_launch.clone(); - for manifest in manifests{ - let key = manifest.get_key(); - let display_name = &manifest.display_name; - let mut safe_open = safe_open_games.contains(display_name) || safe_open_games.contains(&key); - if ui.checkbox(&mut safe_open, display_name).clicked(){ - if safe_open{ - safe_open_games.push(key); - }else{ - safe_open_games.retain(|m| m!= display_name && m!= &key); - } - } - } - epic_settings.safe_launch = safe_open_games; - }) ; - ui.add_space(SECTION_SPACING); - } - } } diff --git a/src/ui/uiapp.rs b/src/ui/uiapp.rs index 3fc6c14..54086b6 100644 --- a/src/ui/uiapp.rs +++ b/src/ui/uiapp.rs @@ -1,17 +1,18 @@ -use std::{collections::HashMap, error::Error}; - -#[cfg(target_family = "unix")] -use crate::heroic::HeroicGameType; +use std::{collections::HashMap, error::Error, time::Duration}; use eframe::{egui, App, Frame}; use egui::{ImageButton, Rounding, Stroke, TextureHandle}; -use steam_shortcuts_util::shortcut::ShortcutOwned; use tokio::{ runtime::Runtime, sync::watch::{self, Receiver}, }; -use crate::{egs::ManifestItem, settings::Settings, sync::SyncProgress, config::get_renames_file}; +use crate::{ + config::get_renames_file, + platforms::{get_platforms, GamesPlatform, Platforms, ShortcutToImport}, + settings::{save_settings, Settings}, + sync::{self, SyncProgress}, +}; use super::{ ui_colors::{ @@ -31,58 +32,78 @@ struct UiImages { save_button: Option, logo_32: Option, } +type GamesToSync = Vec<( + String, + Receiver>>>, +)>; + +pub(crate) fn all_ready(games: &GamesToSync) -> bool { + games.iter().all(|(_name, rx)| rx.borrow().is_some()) +} + +pub(crate) fn get_all_games(games: &GamesToSync) -> Vec<(String, Vec)> { + games + .iter() + .filter_map(|(name, rx)| { + if let FetcStatus::Fetched(Ok(data)) = &*rx.borrow() { + Some((name.to_owned(), data.to_owned())) + } else { + None + } + }) + .collect() +} pub struct MyEguiApp { selected_menu: Menues, pub(crate) settings: Settings, pub(crate) rt: Runtime, ui_images: UiImages, - pub(crate) games_to_sync: Receiver)>>>, + pub(crate) games_to_sync: GamesToSync, pub(crate) status_reciever: Receiver, - pub(crate) epic_manifests: Option>, - #[cfg(target_family = "unix")] - pub(crate) heroic_games: Option>, pub(crate) image_selected_state: ImageSelectState, pub(crate) backup_state: BackupState, pub(crate) disconect_state: DiconnectState, - pub(crate) rename_map : HashMap, - pub(crate) current_edit : Option, + pub(crate) rename_map: HashMap, + pub(crate) current_edit: Option, + pub(crate) platforms: Platforms, } impl MyEguiApp { pub fn new() -> Self { - let runtime = Runtime::new().unwrap(); + let mut runtime = Runtime::new().unwrap(); + let settings = Settings::new().expect("We must be able to load our settings"); + let platforms = get_platforms(); + let games_to_sync = create_games_to_sync(&mut runtime, &platforms); Self { selected_menu: Menues::Import, - settings: Settings::new().expect("We must be able to load our settings"), + settings, rt: runtime, - games_to_sync: watch::channel(FetcStatus::NeedsFetched).1, + games_to_sync, ui_images: UiImages::default(), status_reciever: watch::channel(SyncProgress::NotStarted).1, - epic_manifests: None, - #[cfg(target_family = "unix")] - heroic_games: None, image_selected_state: ImageSelectState::default(), backup_state: BackupState::default(), disconect_state: DiconnectState::default(), rename_map: get_rename_map(), - current_edit: Option::None + current_edit: Option::None, + platforms, } } } -fn get_rename_map() -> HashMap{ +fn get_rename_map() -> HashMap { try_get_rename_map().unwrap_or_default() } -fn try_get_rename_map() -> Result,Box>{ +fn try_get_rename_map() -> Result, Box> { let rename_map = get_renames_file(); let file_content = std::fs::read_to_string(rename_map)?; let deserialized = serde_json::from_str(&file_content)?; Ok(deserialized) } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] enum Menues { Import, Settings, @@ -97,6 +118,23 @@ impl Default for Menues { } } +fn create_games_to_sync(rt: &mut Runtime, platforms: &[Box]) -> GamesToSync { + let mut to_sync = vec![]; + for platform in platforms { + if platform.enabled() { + let (tx, rx) = watch::channel(FetcStatus::NeedsFetched); + to_sync.push((platform.name().to_string(), rx)); + let platform = platform.clone(); + rt.spawn_blocking(move || { + let _ = tx.send(FetcStatus::Fetching); + let games_to_sync = sync::get_platform_shortcuts(platform); + let _ = tx.send(FetcStatus::Fetched(games_to_sync)); + }); + } + } + to_sync +} + impl App for MyEguiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) { let frame = egui::Frame::default() @@ -111,6 +149,8 @@ impl App for MyEguiApp { ui.image(texture, size); ui.add_space(SECTION_SPACING); + let menu_before = self.selected_menu.clone(); + let mut changed = ui .selectable_value(&mut self.selected_menu, Menues::Import, "Import Games") .changed(); @@ -138,9 +178,12 @@ impl App for MyEguiApp { if changed { self.backup_state.available_backups = None; } - if changed && self.selected_menu == Menues::Settings { + if changed + && menu_before == Menues::Settings + && self.selected_menu == Menues::Import + { //We reset games here, since user might change settings - self.games_to_sync = watch::channel(FetcStatus::NeedsFetched).1; + self.games_to_sync = create_games_to_sync(&mut self.rt, &self.platforms); } }); @@ -153,11 +196,11 @@ impl App for MyEguiApp { let save_button = ImageButton::new(texture, size * 0.5); if ui.add(save_button).on_hover_text("Save settings").clicked() { - MyEguiApp::save_settings_to_file(&self.settings.clone()); + save_settings(&self.settings, &self.platforms); } }); } - if self.games_to_sync.borrow().is_some() { + if self.selected_menu == Menues::Import { egui::TopBottomPanel::new(egui::panel::TopBottomSide::Bottom, "Bottom Panel") .frame(frame) .show(ctx, |ui| { @@ -186,14 +229,16 @@ impl App for MyEguiApp { ui.label(&status_string); } } + let all_ready = all_ready(&self.games_to_sync); let texture = self.get_import_image(ui); let size = texture.size_vec2(); let image_button = ImageButton::new(texture, size * 0.5); - if ui - .add(image_button) - .on_hover_text("Import your games into steam") - .clicked() + if all_ready + && ui + .add(image_button) + .on_hover_text("Import your games into steam") + .clicked() && !syncing { self.run_sync(false); @@ -293,6 +338,10 @@ fn setup(ctx: &egui::Context) { } pub fn run_sync() { let mut app = MyEguiApp::new(); + while !all_ready(&app.games_to_sync) { + println!("Finding games, trying again in 500ms"); + std::thread::sleep(Duration::from_secs_f32(0.5)); + } app.run_sync(true); } diff --git a/src/uplay/mod.rs b/src/uplay/mod.rs deleted file mode 100644 index 6188adb..0000000 --- a/src/uplay/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod game; -mod platform; -mod settings; - -pub use platform::Uplay; -pub use settings::UplaySettings; diff --git a/src/uplay/platform.rs b/src/uplay/platform.rs deleted file mode 100644 index 08068d0..0000000 --- a/src/uplay/platform.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::platform::Platform; -use std::error::Error; -#[cfg(target_os = "windows")] -use std::path::Path; -#[cfg(target_os = "windows")] -use std::path::PathBuf; - -use super::{game::Game, settings::UplaySettings}; - -pub struct Uplay { - pub settings: UplaySettings, -} - -impl Platform> for Uplay { - fn enabled(&self) -> bool { - #[cfg(target_family = "unix")] - { - false - } - #[cfg(target_os = "windows")] - { - self.settings.enabled - } - } - - fn name(&self) -> &str { - "Uplay" - } - - fn settings_valid(&self) -> crate::platform::SettingsValidity { - #[cfg(target_family = "unix")] - { - //Linux not supported yet - crate::platform::SettingsValidity::Invalid { - reason: "Linux not supported yet".to_string(), - } - } - #[cfg(target_os = "windows")] - { - if get_launcher_path().is_some() { - crate::platform::SettingsValidity::Valid - } else { - crate::platform::SettingsValidity::Invalid { - reason: "Could not find UPlay instalation".to_string(), - } - } - } - } - - fn get_shortcuts(&self) -> Result, Box> { - #[cfg(target_family = "unix")] - { - Ok(vec![]) - } - #[cfg(target_os = "windows")] - { - get_games_from_winreg() - } - } - - #[cfg(target_family = "unix")] - fn create_symlinks(&self) -> bool { - false - } - - fn needs_proton(&self, _input: &Game) -> bool { - #[cfg(target_os = "windows")] - return false; - #[cfg(target_family = "unix")] - { - //TODO update this when uplay gets proton support on linux - true - } - } -} -#[cfg(target_os = "windows")] -fn get_launcher_path() -> Option { - use winreg::enums::*; - use winreg::RegKey; - - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - if let Ok(launcher_key) = hklm.open_subkey("SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher") { - let launcher_dir: Result = launcher_key.get_value("InstallDir"); - if let Ok(launcher_dir) = launcher_dir { - let path = Path::new(&launcher_dir).join("upc.exe"); - if path.exists() { - return Some(path); - } - } - } - - None -} - -#[cfg(target_os = "windows")] -fn get_games_from_winreg() -> Result, Box> { - use winreg::enums::*; - use winreg::RegKey; - - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - let mut games = vec![]; - let mut installed_ids = vec![]; - let launcher_path = - get_launcher_path().expect("This should only be called if launcher is found"); - - if let Ok(installs) = hklm.open_subkey("SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher\\Installs") { - for i in installs.enum_keys().filter_map(|i| i.ok()) { - if let Ok(install) = installs.open_subkey(&i) { - let install_dir: Result = install.get_value("InstallDir"); - if let Ok(folder) = install_dir { - let path = Path::new(&folder); - if path.exists() { - installed_ids.push(i); - } - } - } - } - } - - for id in installed_ids { - let path = format!("SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Uplay Install {}",id); - let subkey = hklm.open_subkey(path); - if let Ok(subkey) = subkey { - let name: Result = subkey.get_value("DisplayName"); - if let Ok(name) = name { - let icon: String = subkey.get_value("DisplayIcon").unwrap_or_default(); - games.push(Game { - name, - icon, - id, - launcher: launcher_path.clone(), - }) - } - } - } - - Ok(games) -} diff --git a/src/uplay/settings.rs b/src/uplay/settings.rs deleted file mode 100644 index 4c98fb5..0000000 --- a/src/uplay/settings.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct UplaySettings { - pub enabled: bool, -}