Modular platforms (#241)

This commit is contained in:
Philip Kristoffersen
2022-10-02 21:13:31 +02:00
committed by GitHub
parent d6773c7adb
commit dcbf77b610
94 changed files with 2188 additions and 1750 deletions
Generated
+153 -4
View File
@@ -18,6 +18,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e"
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]] [[package]]
name = "adler" name = "adler"
version = "1.0.2" version = "1.0.2"
@@ -121,6 +130,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 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]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@@ -172,11 +196,14 @@ version = "1.5.1"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",
"color-eyre",
"config", "config",
"copypasta", "copypasta",
"dashmap", "dashmap",
"dyn-clone",
"eframe", "eframe",
"egui", "egui",
"eyre",
"flate2", "flate2",
"futures", "futures",
"image", "image",
@@ -368,6 +395,33 @@ dependencies = [
"objc", "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]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@@ -669,6 +723,12 @@ dependencies = [
"wio", "wio",
] ]
[[package]]
name = "dyn-clone"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
[[package]] [[package]]
name = "eframe" name = "eframe"
version = "0.19.0" version = "0.19.0"
@@ -829,6 +889,16 @@ dependencies = [
"threadpool", "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]] [[package]]
name = "fake-simd" name = "fake-simd"
version = "0.1.2" version = "0.1.2"
@@ -1099,6 +1169,12 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
version = "0.14.0" version = "0.14.0"
@@ -1355,6 +1431,12 @@ dependencies = [
"tiff", "tiff",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.8.1"
@@ -1790,9 +1872,9 @@ dependencies = [
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.7" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@@ -1887,6 +1969,15 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.1" version = "1.13.1"
@@ -1972,6 +2063,12 @@ dependencies = [
"ttf-parser", "ttf-parser",
] ]
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.0" version = "0.12.0"
@@ -2302,6 +2399,12 @@ dependencies = [
"ordered-multimap", "ordered-multimap",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]] [[package]]
name = "rusty-leveldb" name = "rusty-leveldb"
version = "1.0.4" version = "1.0.4"
@@ -2479,6 +2582,15 @@ dependencies = [
"opaque-debug", "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]] [[package]]
name = "shared_library" name = "shared_library"
version = "0.1.9" version = "0.1.9"
@@ -2670,9 +2782,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.25.2" version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1594a36887d0f70096702bffadfb67dfbfe76ad4bf84605e86157dc9fce9961a" checksum = "7890fff842b8db56f2033ebee8f6efe1921475c3830c115995552914fb967580"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"core-foundation-sys", "core-foundation-sys",
@@ -2717,6 +2829,15 @@ dependencies = [
"syn", "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]] [[package]]
name = "threadpool" name = "threadpool"
version = "1.8.1" version = "1.8.1"
@@ -2888,6 +3009,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
dependencies = [ dependencies = [
"lazy_static", "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]] [[package]]
@@ -2953,6 +3096,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
+4 -2
View File
@@ -16,8 +16,10 @@ serde_json = "^1.0.83"
sqlite = "^0.27.0" sqlite = "^0.27.0"
steam_shortcuts_util = "^1.1.8" steam_shortcuts_util = "^1.1.8"
steamgriddb_api = "^0.3.1" 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] [dependencies.dashmap]
features = ["serde"] features = ["serde"]
-33
View File
@@ -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<AmazonGame> 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()
}
}
-110
View File
@@ -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<AmazonGame, Box<dyn Error>> 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<Vec<AmazonGame>, Box<dyn Error>> {
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::<String>(0);
let title = statement.read::<String>(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<PathBuf> {
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<PathBuf> {
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,
}
}
-7
View File
@@ -1,7 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AmazonSettings {
pub enabled: bool,
pub launcher_location: Option<String>,
}
-8
View File
@@ -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::*;
-6
View File
@@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BottlesSettings {
pub enabled: bool,
}
+2 -2
View File
@@ -9,7 +9,7 @@ pub fn get_config_folder() -> PathBuf {
let home = std::env::var("HOME"); let home = std::env::var("HOME");
match (config_home, home) { match (config_home, home) {
(Ok(p), _) => Path::new(&p).to_path_buf(), (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(), _ => Path::new("").to_path_buf(),
} }
} }
@@ -49,5 +49,5 @@ pub fn get_backups_flder() -> PathBuf {
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub fn get_boilr_links_path() -> PathBuf { pub fn get_boilr_links_path() -> PathBuf {
get_config_folder().join("links").to_path_buf() get_config_folder().join("links")
} }
-41
View File
@@ -8,47 +8,6 @@ prefer_animated = false
banned_images = [] banned_images = []
only_download_boilr_images = false 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] [steam]
create_collections = false create_collections = false
optimize_for_big_picture = false optimize_for_big_picture = false
-51
View File
@@ -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<ManifestItem, String> for EpicPlatform {
fn enabled(&self) -> bool {
self.settings.enabled
}
fn name(&self) -> &str {
"EGS"
}
fn get_shortcuts(&self) -> Result<Vec<ManifestItem>, 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;
}
}
-5
View File
@@ -1,5 +0,0 @@
mod platform;
mod settings;
pub use platform::*;
pub use settings::*;
-6
View File
@@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct FlatpakSettings {
pub enabled: bool,
}
-10
View File
@@ -1,10 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GogSettings {
pub enabled: bool,
pub location: Option<String>,
pub wine_c_drive: Option<String>,
#[cfg(target_family = "unix")]
pub create_symlinks: bool,
}
-8
View File
@@ -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::*;
-221
View File
@@ -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<HeroicGogPath>,
}
#[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<Vec<HeroicGame>, Box<dyn Error>> {
let installed_path = get_installed_json_location(install_mode);
get_shortcuts_from_location(installed_path)
}
fn get_shortcuts_from_location<P: AsRef<Path>>(path: P) -> Result<Vec<HeroicGame>, Box<dyn Error>> {
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::<HashMap<String, HeroicGame>>(&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<HeroicGameType> {
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<HeroicGameType, Box<dyn Error>> for HeroicPlatform {
fn enabled(&self) -> bool {
self.settings.enabled
}
fn name(&self) -> &str {
"Heroic"
}
fn get_shortcuts(&self) -> Result<Vec<HeroicGameType>, Box<dyn Error>> {
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<HeroicGameType> {
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<Vec<HeroicGameType>, Box<dyn Error>> {
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::<HeroicGogConfig>(&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)
}
-9
View File
@@ -1,9 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ItchSettings {
pub enabled: bool,
pub location: Option<String>,
#[cfg(target_family = "unix")]
pub create_symlinks: bool,
}
-64
View File
@@ -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<LegendaryGame, Box<dyn Error>> for LegendaryPlatform {
fn enabled(&self) -> bool {
self.settings.enabled
}
fn name(&self) -> &str {
"Legendary"
}
fn get_shortcuts(&self) -> Result<Vec<LegendaryGame>, Box<dyn Error>> {
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<Vec<LegendaryGame>, Box<dyn Error>> {
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)
}
-7
View File
@@ -1,7 +0,0 @@
mod legendary_game;
mod legendary_platform;
mod settings;
pub use legendary_game::*;
pub use legendary_platform::*;
pub use settings::*;
-7
View File
@@ -1,7 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LegendarySettings {
pub enabled: bool,
pub executable: Option<String>,
}
-72
View File
@@ -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<LutrisGame, Box<dyn Error>> for LutrisPlatform {
fn enabled(&self) -> bool {
self.settings.enabled
}
fn name(&self) -> &str {
"Lutris"
}
fn get_shortcuts(&self) -> Result<Vec<LutrisGame>, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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())
}
-4
View File
@@ -1,4 +0,0 @@
pub mod game_list_parser;
pub mod lutris_game;
pub mod lutris_platform;
pub mod settings;
-9
View File
@@ -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,
}
+6 -14
View File
@@ -1,25 +1,16 @@
mod amazon;
mod config; mod config;
mod egs;
mod flatpak;
mod gog;
#[cfg(target_family = "unix")]
mod heroic;
mod itch;
mod legendary;
mod lutris;
mod migration; mod migration;
mod origin; mod platforms;
mod platform;
mod settings; mod settings;
mod steam; mod steam;
mod steamgriddb; mod steamgriddb;
mod sync; mod sync;
mod ui; mod ui;
mod uplay;
mod bottles;
fn main() { use color_eyre::eyre::Result;
fn main() -> Result<()>{
color_eyre::install()?;
ensure_config_folder(); ensure_config_folder();
migration::migrate_config(); migration::migrate_config();
@@ -29,6 +20,7 @@ fn main() {
} else { } else {
ui::run_ui(args); ui::run_ui(args);
} }
Ok(())
} }
fn ensure_config_folder() { fn ensure_config_folder() {
+4 -1
View File
@@ -1,5 +1,7 @@
use std::path::Path; use std::path::Path;
use crate::{settings::save_settings, platforms::get_platforms};
pub fn migrate_config() { pub fn migrate_config() {
let version = &crate::settings::Settings::new() let version = &crate::settings::Settings::new()
.map(|s| s.config_version) .map(|s| s.config_version)
@@ -35,7 +37,8 @@ pub fn migrate_config() {
if save_version { if save_version {
if let Ok(mut settings) = crate::settings::Settings::new() { if let Ok(mut settings) = crate::settings::Settings::new() {
settings.config_version = Some(1); settings.config_version = Some(1);
crate::ui::MyEguiApp::save_settings_to_file(&settings); let platforms = get_platforms();
save_settings(&settings, &platforms);
} }
} }
} }
-6
View File
@@ -1,6 +0,0 @@
mod origin_game;
mod origin_platform;
mod settings;
pub use origin_platform::*;
pub use settings::*;
-7
View File
@@ -1,7 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct OriginSettings {
pub enabled: bool,
}
-25
View File
@@ -1,25 +0,0 @@
use steam_shortcuts_util::shortcut::ShortcutOwned;
pub trait Platform<T, E>
where
T: Into<ShortcutOwned>,
{
fn enabled(&self) -> bool;
fn name(&self) -> &str;
fn get_shortcuts(&self) -> Result<Vec<T>, 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 },
}
+154
View File
@@ -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:AsRef<str>>(s:S) -> Self {
AmazonPlatform { settings: load_settings(s) }
}
}
fn get_sqlite_path() -> eyre::Result<PathBuf> {
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<PathBuf> {
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<Vec<ShortcutToImport>> {
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<Vec<AmazonGame>> {
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::<String>(0);
let title = statement.read::<String>(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<AmazonGame> 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<String>,
}
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() }
}
}
+4
View File
@@ -0,0 +1,4 @@
mod amazon_platform;
pub use amazon_platform::AmazonPlatform;
pub use amazon_platform::AmazonGame;
+3
View File
@@ -0,0 +1,3 @@
mod platform;
pub use platform::BottlesPlatform;
@@ -1,24 +1,46 @@
use std::{error::Error, process::Command}; use std::process::Command;
use eframe::epaint::ahash::HashMap; use eframe::epaint::ahash::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::platform::Platform;
use super::BottlesSettings;
use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut};
use crate::platforms::{to_shortcuts_simple, ShortcutToImport, FromSettingsString, load_settings, GamesPlatform};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BottlesPlatform { pub struct BottlesPlatform {
pub settings: BottlesSettings, pub settings: BottlesSettings,
} }
impl FromSettingsString for BottlesPlatform{
fn from_settings_string<S: AsRef<str>>(s: S) -> Self {
BottlesPlatform { settings: load_settings(s) }
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BottlesApp { pub struct BottlesApp {
pub name: String, pub name: String,
pub bottle: 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<BottlesApp> for ShortcutOwned { impl From<BottlesApp> for ShortcutOwned {
fn from(app: BottlesApp) -> Self { fn from(app: BottlesApp) -> Self {
// //
@@ -30,65 +52,17 @@ impl From<BottlesApp> for ShortcutOwned {
} }
} }
impl Platform<BottlesApp, Box<dyn Error>> for BottlesPlatform { fn get_bottles() -> eyre::Result<Vec<Bottle>> {
fn enabled(&self) -> bool { let json = get_bottles_output()?;
self.settings.enabled let map: HashMap<String, Bottle> = serde_json::from_str(json.as_str())?;
} let mut res = vec![];
for (_, value) in map {
fn name(&self) -> &str { res.push(value);
"Bottles"
}
fn get_shortcuts(&self) -> Result<Vec<BottlesApp>, Box<dyn Error>> {
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
} }
Ok(res)
} }
fn get_bottles() -> Vec<Bottle> { fn get_bottles_output() -> eyre::Result<String> {
let output_result = get_bottles_output();
match output_result {
Ok(json) => {
let map : HashMap<String,Bottle>= 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<String, Box<dyn Error>> {
let output = { let output = {
#[cfg(not(feature = "flatpak"))] #[cfg(not(feature = "flatpak"))]
{ {
@@ -120,17 +94,60 @@ fn get_bottles_output() -> Result<String, Box<dyn Error>> {
Ok(String::from_utf8_lossy(&output.stdout).to_string()) Ok(String::from_utf8_lossy(&output.stdout).to_string())
} }
#[derive(Deserialize,Debug)] #[derive(Deserialize, Debug)]
struct Bottle { struct Bottle {
#[serde(alias = "Name")] #[serde(alias = "Name")]
pub(crate) name: String, pub(crate) name: String,
#[serde(alias = "External_Programs")] #[serde(alias = "External_Programs")]
pub(crate) external_programs : HashMap<String,Program>, pub(crate) external_programs: HashMap<String, Program>,
} }
#[derive(Deserialize,Debug)] #[derive(Deserialize, Debug)]
struct Program { struct Program {
#[serde(alias = "Name")] #[serde(alias = "Name")]
pub(crate) name: String, pub(crate) name: String,
} }
impl BottlesPlatform {
fn get_botttles(&self) -> eyre::Result<Vec<BottlesApp>> {
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<Vec<ShortcutToImport>> {
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"
}
}
+59
View File
@@ -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<Vec<ManifestItem>>,
}
impl FromSettingsString for EpicPlatform {
fn from_settings_string<S: AsRef<str>>(s: S) -> Self {
EpicPlatform {
settings: load_settings(s),
epic_manifests: None,
}
}
}
impl NeedsPorton<EpicPlatform> 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<Vec<ShortcutToImport>> {
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()
}
}
+40
View File
@@ -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;
});
}
}
}
@@ -1,4 +1,5 @@
use super::{EpicGamesLauncherSettings, ManifestItem}; use super::settings::EpicGamesLauncherSettings;
use super::ManifestItem;
use std::fs::{DirEntry, File}; use std::fs::{DirEntry, File};
use std::io::BufReader; use std::io::BufReader;
@@ -6,8 +7,8 @@ use std::path::Path;
pub(crate) fn get_egs_manifests( pub(crate) fn get_egs_manifests(
settings: &EpicGamesLauncherSettings, settings: &EpicGamesLauncherSettings,
) -> Result<Vec<ManifestItem>, String> { ) -> eyre::Result<Vec<ManifestItem>> {
let locations = crate::egs::get_locations(); let locations = super::get_locations();
match locations { match locations {
Some(locations) => { Some(locations) => {
let manifest_dir_path = locations.manifest_folder_path; let manifest_dir_path = locations.manifest_folder_path;
@@ -15,14 +16,12 @@ pub(crate) fn get_egs_manifests(
match manifest_dir_result { match manifest_dir_result {
Ok(manifest_dir) => { Ok(manifest_dir) => {
let all_manifests = manifest_dir let mut all_manifests = manifest_dir
.filter_map(|dir| dir.ok()) .filter_map(|dir| dir.ok())
.filter_map(get_manifest_item); .filter_map(get_manifest_item);
let mut manifests = vec![]; #[cfg(target_family = "unix")]
for mut manifest in all_manifests { {
manifest.manifest_location = manifest.manifest_location; for mut manifest in &mut all_manifests {
#[cfg(target_family = "unix")]
{
if let Some(compat_folder) = locations.compat_folder_path.as_ref() { if let Some(compat_folder) = locations.compat_folder_path.as_ref() {
//Strip off the c:\\ //Strip off the c:\\
manifest.manifest_location = compat_folder manifest.manifest_location = compat_folder
@@ -42,23 +41,14 @@ pub(crate) fn get_egs_manifests(
.to_string(); .to_string();
} }
} }
if is_game_installed(&manifest) && is_game_launchable(&manifest) {
manifests.push(manifest);
}
} }
let mut manifests :Vec<ManifestItem> = all_manifests
.filter(is_game_installed)
.filter(is_game_launchable)
.collect();
manifests.sort_by_key(|m| { manifests.sort_by_key(|m| m.dedupe_key());
format!( manifests.dedup_by_key(|m| m.dedupe_key());
"{}-{}-{}",
m.install_location, m.launch_executable, m.is_managed
)
});
manifests.dedup_by_key(|m| {
format!(
"{}-{}-{}",
m.install_location, m.launch_executable, m.is_managed
)
});
for mut manifest in &mut manifests { for mut manifest in &mut manifests {
manifest.launcher_path = Some(locations.launcher_path.clone()); manifest.launcher_path = Some(locations.launcher_path.clone());
manifest.compat_folder = locations.compat_folder_path.clone(); manifest.compat_folder = locations.compat_folder_path.clone();
@@ -70,16 +60,18 @@ pub(crate) fn get_egs_manifests(
} }
Ok(manifests) Ok(manifests)
} }
Err(err) => Err(format!( Err(err) => Err(eyre::format_err!(
"Could not read dir at: {:?} error: {:?}", "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 { fn is_game_installed(manifest: &ManifestItem) -> bool {
Path::new(manifest.manifest_location.as_str()).exists() Path::new(manifest.manifest_location.as_str()).exists()
} }
@@ -124,6 +124,7 @@ fn launcher_shortcut(manifest: ManifestItem) -> ShortcutOwned {
.to_owned() .to_owned()
} }
impl From<ManifestItem> for ShortcutOwned { impl From<ManifestItem> for ShortcutOwned {
fn from(manifest: ManifestItem) -> Self { fn from(manifest: ManifestItem) -> Self {
let mut owned_shortcut = if manifest.needs_launcher() { let mut owned_shortcut = if manifest.needs_launcher() {
@@ -162,6 +163,13 @@ impl ManifestItem {
self.catalog_namespace, self.catalog_item_id, self.app_name 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 { fn needs_launcher(&self) -> bool {
if self.safe_launch { if self.safe_launch {
+3 -2
View File
@@ -3,9 +3,10 @@ mod get_manifests;
mod manifest_item; mod manifest_item;
mod paths; mod paths;
mod settings; mod settings;
mod epic_ui;
pub use epic_platform::*; pub use epic_platform::*;
use get_manifests::get_egs_manifests; use get_manifests::get_egs_manifests;
pub(crate) use manifest_item::*; pub(crate) use manifest_item::*;
pub use paths::*; use paths::*;
pub use settings::EpicGamesLauncherSettings;
@@ -5,3 +5,12 @@ pub struct EpicGamesLauncherSettings {
pub enabled: bool, pub enabled: bool,
pub safe_launch: Vec<String>, pub safe_launch: Vec<String>,
} }
impl Default for EpicGamesLauncherSettings {
fn default() -> Self {
Self {
enabled: true,
safe_launch: Default::default(),
}
}
}
@@ -2,4 +2,4 @@ mod platform;
mod settings; mod settings;
pub use platform::*; pub use platform::*;
pub use settings::*; pub(crate) use settings::*;
@@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::platform::{Platform, SettingsValidity}; use crate::platforms::{NeedsPorton, to_shortcuts, ShortcutToImport, GamesPlatform, FromSettingsString, load_settings};
use std::error::Error;
use super::FlatpakSettings; use super::FlatpakSettings;
use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut};
@@ -24,16 +23,19 @@ impl From<FlatpakApp> for ShortcutOwned {
} }
} }
impl Platform<FlatpakApp, Box<dyn Error>> for FlatpakPlatform { impl NeedsPorton<FlatpakPlatform> for FlatpakApp{
fn enabled(&self) -> bool { fn needs_proton(&self, _platform: &FlatpakPlatform) -> bool {
self.settings.enabled false
} }
fn name(&self) -> &str { fn create_symlinks(&self, _platform: &FlatpakPlatform) -> bool {
"Flatpak" false
} }
}
fn get_shortcuts(&self) -> Result<Vec<FlatpakApp>, Box<dyn Error>> { impl FlatpakPlatform {
fn get_flatpak_apps(&self) -> eyre::Result<Vec<FlatpakApp>> {
use std::process::Command; use std::process::Command;
let mut command = Command::new("flatpak"); let mut command = Command::new("flatpak");
let output = command let output = command
@@ -56,23 +58,41 @@ impl Platform<FlatpakApp, Box<dyn Error>> for FlatpakPlatform {
} }
Ok(result) Ok(result)
} }
}
fn settings_valid(&self) -> crate::platform::SettingsValidity {
let shortcuts_res = self.get_shortcuts(); impl FromSettingsString for FlatpakPlatform{
match shortcuts_res { fn from_settings_string<S: AsRef<str>>(s: S) -> Self {
Ok(_) => SettingsValidity::Valid, FlatpakPlatform {
Err(err) => SettingsValidity::Invalid { settings: load_settings(s),
reason: format!("{}", err),
},
} }
} }
}
#[cfg(target_family = "unix")] impl GamesPlatform for FlatpakPlatform{
fn create_symlinks(&self) -> bool { fn name(&self) -> &str {
false "Flatpak"
}
fn enabled(&self) -> bool {
self.settings.enabled
} }
fn needs_proton(&self, _input: &FlatpakApp) -> bool { fn get_shortcut_info(&self) -> eyre::Result<Vec<ShortcutToImport>> {
false 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"
}
}
+18
View File
@@ -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 }
}
}
@@ -1,20 +1,44 @@
use std::path::{Path, PathBuf}; 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::{ use super::{
gog_config::GogConfig,
gog_game::{GogGame, GogShortcut}, gog_game::{GogGame, GogShortcut},
GogSettings, GogSettings,
}; };
#[derive(Clone)]
pub struct GogPlatform { pub struct GogPlatform {
pub settings: GogSettings, pub settings: GogSettings,
} }
pub fn get_shortcuts_from_config( impl GogPlatform {
fn get_shortcuts(&self) -> eyre::Result<Vec<GogShortcut>> {
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<String>, _wine_c_drive: Option<String>,
config_path: PathBuf, config_path: PathBuf,
) -> Result<Vec<GogShortcut>, String> { ) -> eyre::Result<Vec<GogShortcut>> {
let install_locations = get_install_locations(config_path)?; let install_locations = get_install_locations(config_path)?;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let install_locations = if let Some(wine_c_drive) = &_wine_c_drive { 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) Ok(shortcuts)
} }
pub fn get_shortcuts_from_game_folders(game_folders: Vec<PathBuf>) -> Vec<GogShortcut> { pub fn get_gog_shortcuts_from_game_folders(game_folders: Vec<PathBuf>) -> Vec<GogShortcut> {
let games = get_games_from_game_folders(game_folders); let games = get_games_from_game_folders(game_folders);
get_shortcuts_from_games(games) get_shortcuts_from_games(games)
@@ -137,53 +161,25 @@ fn get_games_from_game_folders(game_folders: Vec<PathBuf>) -> Vec<(GogGame, Path
games games
} }
impl Platform<GogShortcut, String> for GogPlatform { impl NeedsPorton<GogPlatform> for GogShortcut{
fn enabled(&self) -> bool { #[cfg(target_family = "unix")]
self.settings.enabled fn needs_proton(&self, _platform: &GogPlatform) -> bool {
true
} }
fn name(&self) -> &str { #[cfg(not(target_family = "unix"))]
"Gog" 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")] #[cfg(target_family = "unix")]
fn create_symlinks(&self) -> bool { fn create_symlinks(&self, platform: &GogPlatform) -> bool {
self.settings.create_symlinks platform.settings.create_symlinks
}
fn get_shortcuts(&self) -> Result<Vec<GogShortcut>, 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;
} }
} }
@@ -202,11 +198,9 @@ fn fix_paths(wine_c_drive: &str, paths: Vec<String>) -> Vec<String> {
.collect() .collect()
} }
fn get_install_locations(path: PathBuf) -> Result<Vec<String>, String> { fn get_install_locations(path: PathBuf) -> eyre::Result<Vec<String>> {
let data_res = std::fs::read_to_string(&path) 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)?;
let config: GogConfig = serde_json::from_str(&data_res)
.map_err(|e| format!("Config file not read {:?} , error: {:?} ", path.clone(), e))?;
let path_vec = match config.library_path { let path_vec = match config.library_path {
Some(path) => vec![path], Some(path) => vec![path],
None => vec![], None => vec![],
@@ -227,3 +221,49 @@ pub fn default_location() -> PathBuf {
Path::new(&home).join("Games/gog-galaxy/drive_c/ProgramData/GOG.com/Galaxy") Path::new(&home).join("Games/gog-galaxy/drive_c/ProgramData/GOG.com/Galaxy")
} }
} }
impl FromSettingsString for GogPlatform{
fn from_settings_string<S: AsRef<str>>(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<Vec<ShortcutToImport>> {
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"
}
}
+25
View File
@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GogSettings {
pub enabled: bool,
pub location: Option<String>,
pub wine_c_drive: Option<String>,
#[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,
}
}
}
+9
View File
@@ -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;
@@ -1,13 +1,12 @@
use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut};
use super::{HeroicGame, InstallationMode}; use super::{HeroicGame, InstallationMode};
use crate::gog::GogShortcut;
#[derive(Clone)] #[derive(Clone)]
pub enum HeroicGameType { pub enum HeroicGameType {
Epic(HeroicGame), Epic(HeroicGame),
//The bool is if it is windows (true) or not (false) //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 //The string is the app name
Heroic { Heroic {
title: String, title: String,
+285
View File
@@ -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<Vec<HeroicGameType>>,
}
#[derive(Deserialize, Debug, Clone, Copy)]
pub enum InstallationMode {
FlatPak,
UserBin,
}
#[derive(Deserialize)]
struct HeroicGogConfig {
installed: Vec<HeroicGogPath>,
}
#[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<Vec<HeroicGame>> {
let installed_path = get_installed_json_location(install_mode);
get_shortcuts_from_location(installed_path)
}
fn get_shortcuts_from_location<P: AsRef<Path>>(path: P) -> eyre::Result<Vec<HeroicGame>> {
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::<HashMap<String, HeroicGame>>(&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<Vec<HeroicGameType>> {
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<HeroicPlatform> 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<Vec<HeroicGameType>> {
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<Vec<HeroicGameType>> {
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::<HeroicGogConfig>(&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: AsRef<str>>(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<Vec<ShortcutToImport>> {
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"
}
}
@@ -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,
}
}
}
@@ -67,7 +67,7 @@ mod tests {
#[test] #[test]
fn parse_itch_butler_db_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); let result = parse_butler_db(content);
assert!(result.is_ok()); assert!(result.is_ok());
let (_r, paths) = result.unwrap(); let (_r, paths) = result.unwrap();
@@ -98,7 +98,7 @@ mod tests {
#[test] #[test]
fn parse_itch_butler_db_test_other() { 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); let result = parse_butler_db(content);
assert!(result.is_ok()); assert!(result.is_ok());
let (_r, paths) = result.unwrap(); let (_r, paths) = result.unwrap();
@@ -3,6 +3,10 @@ use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut};
use crate::platforms::NeedsPorton;
use super::ItchPlatform;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ItchGame { pub struct ItchGame {
@@ -33,3 +37,21 @@ impl From<ItchGame> for ShortcutOwned {
owned_shortcut owned_shortcut
} }
} }
impl NeedsPorton<ItchPlatform> 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
}
}
@@ -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::receipt::Receipt;
use super::{ItchGame, ItchSettings}; use super::ItchSettings;
use crate::platform::{Platform, SettingsValidity}; use super::{butler_db_parser::*};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use is_executable::IsExecutable; use is_executable::IsExecutable;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::Path; use std::path::Path;
#[derive(Clone)]
pub struct ItchPlatform { pub struct ItchPlatform {
settings: ItchSettings, pub settings: ItchSettings,
} }
impl ItchPlatform { impl ItchPlatform {
pub fn new(settings: ItchSettings) -> ItchPlatform { fn get_itch_games(&self) -> eyre::Result<Vec<ItchGame>> {
ItchPlatform { settings }
}
}
impl Platform<ItchGame, String> for ItchPlatform {
fn enabled(&self) -> bool {
self.settings.enabled
}
fn name(&self) -> &str {
"Itch"
}
fn get_shortcuts(&self) -> Result<Vec<ItchGame>, String> {
let itch_location = self.settings.location.clone(); let itch_location = self.settings.location.clone();
let itch_location = itch_location.unwrap_or_else(get_default_location); 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"); let itch_db_location = Path::new(&itch_location).join("db").join("butler.db-wal");
if !itch_db_location.exists() { 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 shortcut_bytes = std::fs::read(&itch_db_location).unwrap();
let paths = match parse_butler_db(&shortcut_bytes) { let paths = match parse_butler_db(&shortcut_bytes) {
Ok((_, shortcuts)) => Ok(shortcuts), Ok((_, shortcuts)) => Ok(shortcuts),
Err(e) => Err(format!( Err(e) => Err(eyre::format_err!(
"Could not parse path: {:?} , error: {:?}", "Could not parse path: {:?} , error: {:?}",
itch_db_location.to_str(), itch_db_location.to_str(),
e e
@@ -52,31 +43,6 @@ impl Platform<ItchGame, String> for ItchPlatform {
let res = paths.iter().filter_map(|e| dbpath_to_game(*e)).collect(); let res = paths.iter().filter_map(|e| dbpath_to_game(*e)).collect();
Ok(res) 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<ItchGame> { fn dbpath_to_game(paths: &DbPaths) -> Option<ItchGame> {
@@ -86,13 +52,11 @@ fn dbpath_to_game(paths: &DbPaths) -> Option<ItchGame> {
if !&recipt.exists() { if !&recipt.exists() {
return None; return None;
} }
paths
let executable = paths
.paths .paths
.iter() .iter()
.find(|p| Path::new(&paths.base_path).join(&p).is_executable()); .filter(|p| Path::new(&paths.base_path).join(&p).is_executable())
match executable { .find_map(|executable| {
Some(executable) => {
let gz_bytes = std::fs::read(&recipt).unwrap(); let gz_bytes = std::fs::read(&recipt).unwrap();
let mut d = GzDecoder::new(gz_bytes.as_slice()); let mut d = GzDecoder::new(gz_bytes.as_slice());
let mut s = String::new(); let mut s = String::new();
@@ -104,9 +68,7 @@ fn dbpath_to_game(paths: &DbPaths) -> Option<ItchGame> {
executable: executable.to_owned(), executable: executable.to_owned(),
title: re.game.title, title: re.game.title,
}) })
} })
None => None,
}
} }
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
@@ -125,5 +87,70 @@ pub fn get_default_location() -> String {
.to_str() .to_str()
.unwrap() .unwrap()
.to_string() .to_string()
//C:\Users\phili\AppData\Local\itch //C:\Users\phili\AppData\Local\itch
}
impl FromSettingsString for ItchPlatform {
fn from_settings_string<S: AsRef<str>>(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<Vec<ShortcutToImport>> {
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"
}
} }
@@ -4,6 +4,5 @@ mod itch_platform;
mod receipt; mod receipt;
mod settings; mod settings;
pub use itch_game::*; pub use itch_platform::ItchPlatform;
pub use itch_platform::*; pub use settings::ItchSettings;
pub use settings::*;
+20
View File
@@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ItchSettings {
pub enabled: bool,
pub location: Option<String>,
#[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,
}
}
}
@@ -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<Vec<LegendaryGame>> {
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<Vec<LegendaryGame>> {
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<Vec<ShortcutToImport>> {
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: AsRef<str>>(s: S) -> Self {
LegendaryPlatform{
settings:load_settings(s)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
mod legendary_game;
mod legendary_platform;
mod settings;
pub use settings::LegendarySettings;
pub use legendary_platform::LegendaryPlatform;
+20
View File
@@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LegendarySettings {
pub enabled: bool,
pub executable: Option<String>,
}
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(),
}
}
}
@@ -1,8 +1,9 @@
use crate::lutris::settings::LutrisSettings;
use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut};
use serde::Deserialize; use serde::Deserialize;
use super::LutrisSettings;
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct LutrisGame { pub struct LutrisGame {
pub id: i64, pub id: i64,
+110
View File
@@ -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<Vec<LutrisGame>> {
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<String> {
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: AsRef<str>>(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<Vec<ShortcutToImport>> {
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"
}
}
+7
View File
@@ -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;
+26
View File
@@ -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(),
}
}
}
+22
View File
@@ -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;
+6
View File
@@ -0,0 +1,6 @@
mod origin_game;
mod origin_platform;
mod settings;
pub use settings::OriginSettings;
pub(crate) use origin_platform::OriginPlatform;
@@ -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 nom::bytes::complete::take_until;
use std::{ use std::{
fs::DirEntry, fs::DirEntry,
@@ -7,39 +7,37 @@ use std::{
use super::{origin_game::OriginGame, OriginSettings}; use super::{origin_game::OriginGame, OriginSettings};
#[derive(Clone)]
pub struct OriginPlatform { pub struct OriginPlatform {
pub settings: OriginSettings, pub settings: OriginSettings,
} }
impl Platform<OriginGame, String> for OriginPlatform { impl NeedsPorton<OriginPlatform> for OriginGame {
fn enabled(&self) -> bool { #[cfg(target_os = "windows")]
self.settings.enabled fn needs_proton(&self, _platform: &OriginPlatform) -> bool {
}
fn name(&self) -> &str {
"Origin"
}
#[cfg(target_family = "unix")]
fn create_symlinks(&self) -> bool {
false false
} }
fn get_shortcuts(&self) -> Result<Vec<OriginGame>, 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<Vec<OriginGame>> {
let origin_folders = get_default_locations(); let origin_folders = get_default_locations();
if origin_folders.is_none() { 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_folders = origin_folders.unwrap();
let origin_folder = origin_folders.local_content_path; let origin_folder = origin_folders.local_content_path;
let origin_exe = origin_folders.exe_path; let origin_exe = origin_folders.exe_path;
let game_folders = origin_folder.join("LocalContent").read_dir().map_err(|e| { let game_folders = origin_folder.join("LocalContent").read_dir()?;
format!(
"Could not read game dir: {} , error: {:?}",
origin_folder.join("LocalContent").to_string_lossy(),
e
)
})?;
let games = game_folders let games = game_folders
.filter_map(|folder| folder.ok()) .filter_map(|folder| folder.ok())
.filter_map(|game_folder| { .filter_map(|game_folder| {
@@ -60,25 +58,6 @@ impl Platform<OriginGame, String> for OriginPlatform {
}); });
Ok(games.collect()) 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<String> { fn get_folder_mfst_file_content(game_folder_path: &Path) -> Option<String> {
@@ -200,3 +179,39 @@ fn get_exe_path() -> Option<PathBuf> {
} }
None None
} }
impl GamesPlatform for OriginPlatform {
fn name(&self) -> &str {
"Origin"
}
fn enabled(&self) -> bool {
self.settings.enabled
}
fn get_shortcut_info(&self) -> eyre::Result<Vec<ShortcutToImport>> {
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: AsRef<str>>(s: S) -> Self {
OriginPlatform { settings: load_settings(s) }
}
}
+13
View File
@@ -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 }
}
}
+82
View File
@@ -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<Vec<ShortcutToImport>>;
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<T, P>(
platform: &P,
into_shortcuts: Result<Vec<T>, eyre::ErrReport>,
) -> eyre::Result<Vec<ShortcutToImport>>
where
T: Into<ShortcutOwned>,
T: NeedsPorton<P>,
{
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<T>(
into_shortcuts: Result<Vec<T>, eyre::ErrReport>,
) -> eyre::Result<Vec<ShortcutToImport>>
where
T: Into<ShortcutOwned>,
{
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<P> {
fn needs_proton(&self, platform: &P) -> bool;
fn create_symlinks(&self, platform: &P) -> bool;
}
+115
View File
@@ -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<Box<dyn GamesPlatform>>;
pub fn load_platform<A: AsRef<str>, B: AsRef<str>>(
name: A,
settings_string: B,
) -> eyre::Result<Box<dyn GamesPlatform>> {
let name = name.as_ref();
let s = settings_string.as_ref();
match name {
"amazon" => load::<AmazonPlatform>(s),
"bottles" => load::<BottlesPlatform>(s),
"epic_games" => load::<EpicPlatform>(s),
"uplay" => load::<UplayPlatform>(s),
"itch" => load::<ItchPlatform>(s),
"flatpak" => load::<FlatpakPlatform>(s),
"gog" => load::<GogPlatform>(s),
"heroic" => load::<HeroicPlatform>(s),
"legendary" => load::<LegendaryPlatform>(s),
"lutris" => load::<LutrisPlatform>(s),
"origin" => load::<OriginPlatform>(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<Setting, S: AsRef<str>>(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<T>(s: &str) -> eyre::Result<Box<dyn GamesPlatform>>
where
T: FromSettingsString,
T: GamesPlatform,
T: 'static,
{
Ok(Box::new(T::from_settings_string(s)))
}
pub trait FromSettingsString {
fn from_settings_string<S: AsRef<str>>(s: S) -> Self;
}
@@ -3,15 +3,15 @@ use std::path::{Path, PathBuf};
use steam_shortcuts_util::shortcut::{Shortcut, ShortcutOwned}; use steam_shortcuts_util::shortcut::{Shortcut, ShortcutOwned};
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Game { pub(crate) struct UplayGame {
pub(crate) name: String, pub(crate) name: String,
pub(crate) icon: String, pub(crate) icon: String,
pub(crate) id: String, pub(crate) id: String,
pub(crate) launcher: PathBuf, pub(crate) launcher: PathBuf,
} }
impl From<Game> for ShortcutOwned { impl From<UplayGame> for ShortcutOwned {
fn from(game: Game) -> Self { fn from(game: UplayGame) -> Self {
let launch = format!("\"uplay://launch/{}/0\"", game.id); let launch = format!("\"uplay://launch/{}/0\"", game.id);
let start_dir = game let start_dir = game
.launcher .launcher
@@ -21,4 +21,4 @@ impl From<Game> for ShortcutOwned {
let exe = format!("\"{}\"", game.launcher.to_string_lossy()); let exe = format!("\"{}\"", game.launcher.to_string_lossy());
Shortcut::new("0", &game.name, &exe, &start_dir, &game.icon, "", &launch).to_owned() Shortcut::new("0", &game.name, &exe, &start_dir, &game.icon, "", &launch).to_owned()
} }
} }
+6
View File
@@ -0,0 +1,6 @@
mod game;
mod platform;
mod settings;
pub use platform::UplayPlatform;
pub use settings::UplaySettings;
+128
View File
@@ -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<Vec<UplayGame>> {
#[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<PathBuf> {
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<Vec<UplayGame>> {
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<String, _> = 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<String, _> = 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: AsRef<str>>(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<Vec<ShortcutToImport>> {
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"
}
}
+16
View File
@@ -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 }
}
}
+60 -25
View File
@@ -1,40 +1,19 @@
use crate::{ use crate::{
amazon::AmazonSettings, config::get_config_file, egs::EpicGamesLauncherSettings, config::get_config_file, platforms::Platforms, steam::SteamSettings,
flatpak::FlatpakSettings, gog::GogSettings, itch::ItchSettings, legendary::LegendarySettings, steamgriddb::SteamGridDbSettings,
lutris::settings::LutrisSettings, origin::OriginSettings, steam::SteamSettings,
steamgriddb::SteamGridDbSettings, uplay::UplaySettings,
}; };
#[cfg(target_family = "unix")]
use crate::heroic::HeroicSettings;
#[cfg(target_family = "unix")]
use crate::bottles::BottlesSettings;
use config::{Config, ConfigError, Environment, File}; use config::{Config, ConfigError, Environment, File};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::env; use std::{collections::HashMap, env};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Settings { pub struct Settings {
pub debug: bool, pub debug: bool,
pub config_version: Option<usize>, pub config_version: Option<usize>,
pub blacklisted_games: Vec<u32>, pub blacklisted_games: Vec<u32>,
pub epic_games: EpicGamesLauncherSettings,
pub legendary: LegendarySettings,
pub itch: ItchSettings,
pub steamgrid_db: SteamGridDbSettings, pub steamgrid_db: SteamGridDbSettings,
pub steam: SteamSettings, 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 { impl Settings {
@@ -65,10 +44,66 @@ impl Settings {
} }
} }
pub fn load_setting_sections() -> eyre::Result<HashMap<String, String>> {
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<String> = vec![];
let mut current_section_name: Option<String> = Option::None;
for line in lines {
if line.starts_with('[') && line.ends_with(']') {
add_sections(&current_section_name, &current_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(&current_section_name, &current_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<String>,
current_section_lines: &Vec<String>,
result: &mut HashMap<String, String>,
) {
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) { fn sanitize_auth_key(result: &mut Settings) {
if let Some(auth_key) = result.steamgrid_db.auth_key.as_ref() { if let Some(auth_key) = result.steamgrid_db.auth_key.as_ref() {
if auth_key == "Write your authentication key between these quotes" { if auth_key == "Write your authentication key between these quotes" {
result.steamgrid_db.auth_key = None; result.steamgrid_db.auth_key = None;
} }
} }
} }
+2
View File
@@ -1,5 +1,6 @@
mod collections; mod collections;
mod installed_games; mod installed_games;
#[cfg(target_family = "unix")]
mod proton_vdf_util; mod proton_vdf_util;
mod restarter; mod restarter;
mod settings; mod settings;
@@ -7,6 +8,7 @@ mod utils;
pub use collections::*; pub use collections::*;
pub use installed_games::*; pub use installed_games::*;
#[cfg(target_family = "unix")]
pub use proton_vdf_util::*; pub use proton_vdf_util::*;
pub use restarter::*; pub use restarter::*;
pub use settings::SteamSettings; pub use settings::SteamSettings;
+1 -1
View File
@@ -43,7 +43,7 @@ pub struct ShortcutInfo {
pub shortcuts: Vec<ShortcutOwned>, pub shortcuts: Vec<ShortcutOwned>,
} }
#[derive(Default, PartialEq, Clone, Debug)] #[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct SteamUsersInfo { pub struct SteamUsersInfo {
pub steam_user_data_folder: String, pub steam_user_data_folder: String,
pub shortcut_path: Option<String>, pub shortcut_path: Option<String>,
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageType { pub enum ImageType {
Hero, Hero,
Grid, Grid,
+3 -3
View File
@@ -1,11 +1,11 @@
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
mod symlinks; pub mod symlinks;
mod synchronization; mod synchronization;
pub use synchronization::download_images; pub use synchronization::download_images;
pub use synchronization::get_platform_shortcuts;
pub use synchronization::run_sync;
pub use synchronization::IsBoilRShortcut; pub use synchronization::IsBoilRShortcut;
pub use synchronization::SyncProgress; pub use synchronization::SyncProgress;
pub use synchronization::*; pub use synchronization::*;
+22 -140
View File
@@ -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 tokio::sync::watch::Sender;
use crate::{ use crate::{
egs::EpicPlatform, platforms::{GamesPlatform, ShortcutToImport},
legendary::LegendaryPlatform,
lutris::lutris_platform::LutrisPlatform,
platform::Platform,
settings::Settings, settings::Settings,
steam::{ 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, Collection, ShortcutInfo, SteamUsersInfo,
}, },
steamgriddb::{download_images_for_users, ImageType}, steamgriddb::{download_images_for_users, ImageType},
uplay::Uplay,
}; };
#[cfg(target_family = "unix")] use std::{collections::HashMap, error::Error};
use crate::heroic::HeroicPlatform;
#[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}; use std::{fs::File, io::Write, path::Path};
pub const BOILR_TAG: &str = "boilr"; pub const BOILR_TAG: &str = "boilr";
@@ -56,30 +47,29 @@ pub fn disconnect_shortcut(settings: &Settings, app_id: u32) -> Result<(), Strin
Ok(()) Ok(())
} }
pub fn run_sync( pub fn sync_shortcuts(
settings: &Settings, settings: &Settings,
platform_shortcuts: &[(String, Vec<ShortcutOwned>)],
sender: &mut Option<Sender<SyncProgress>>, sender: &mut Option<Sender<SyncProgress>>,
renames: &HashMap<u32,String> renames: &HashMap<u32, String>,
) -> Result<Vec<SteamUsersInfo>, String> { ) -> Result<Vec<SteamUsersInfo>, String> {
if let Some(sender) = &sender {
let _ = sender.send(SyncProgress::Starting);
}
let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam) let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam)
.map_err(|e| format!("Getting shortcut paths failed: {e}"))?; .map_err(|e| format!("Getting shortcut paths failed: {e}"))?;
let platform_shortcuts = get_platform_shortcuts(settings);
let mut all_shortcuts: Vec<ShortcutOwned> = platform_shortcuts let mut all_shortcuts: Vec<ShortcutOwned> = platform_shortcuts
.iter() .iter()
.flat_map(|s| s.1.clone()) .flat_map(|s| s.1.clone())
.filter(|s| !settings.blacklisted_games.contains(&s.app_id)) .filter(|s| !settings.blacklisted_games.contains(&s.app_id))
.collect(); .collect();
for shortcut in &mut all_shortcuts{
shortcut.dev_kit_game_id = BOILR_TAG.to_string();
}
if let Some(sender) = &sender { if let Some(sender) = &sender {
let _ = sender.send(SyncProgress::FoundGames { let _ = sender.send(SyncProgress::FoundGames {
games_found: all_shortcuts.len(), games_found: all_shortcuts.len(),
}); });
} }
for shortcut in &mut all_shortcuts { 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(); shortcut.app_name = rename.clone();
let new_shortcut = Shortcut::new( let new_shortcut = Shortcut::new(
"0", "0",
@@ -88,7 +78,7 @@ pub fn run_sync(
"", "",
"", "",
"", "",
"" "",
); );
shortcut.app_id = calculate_app_id_for_shortcut(&new_shortcut); 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)); save_shortcuts(&shortcut_info.shortcuts, Path::new(&shortcut_info.path));
if settings.steam.create_collections { if settings.steam.create_collections {
match write_shortcut_collections(&user.user_id, &platform_shortcuts) { match write_shortcut_collections(&user.user_id, platform_shortcuts) {
Ok(_) => (), Ok(_) => (),
Err(_e) => eprintln!("Could not write collections, make sure steam is shut down"), 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(); let duration = start_time.elapsed();
println!("Finished synchronizing games in: {:?}", duration); println!("Finished synchronizing games in: {:?}", duration);
} }
Ok(userinfo_shortcuts) Ok(userinfo_shortcuts)
} }
@@ -211,47 +200,14 @@ fn write_shortcut_collections<S: AsRef<str>>(
Ok(()) Ok(())
} }
pub fn get_platform_shortcuts(settings: &Settings) -> Vec<(String, Vec<ShortcutOwned>)> { pub fn get_platform_shortcuts(
let mut platform_results = vec![ platform: Box<dyn GamesPlatform>,
update_platform_shortcuts(&EpicPlatform::new(&settings.epic_games)), ) -> eyre::Result<Vec<ShortcutToImport>> {
update_platform_shortcuts(&LegendaryPlatform::new(settings.legendary.clone())), if platform.enabled() {
update_platform_shortcuts(&ItchPlatform::new(settings.itch.clone())), platform.get_shortcut_info()
update_platform_shortcuts(&OriginPlatform { }else{
settings: settings.origin.clone(), Ok(vec![])
}),
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(),
}));
} }
#[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) { fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) {
@@ -280,77 +236,3 @@ fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) {
} }
} }
} }
fn update_platform_shortcuts<P, T, E>(platform: &P) -> Option<(String, Vec<ShortcutOwned>)>
where
P: Platform<T, E>,
E: std::fmt::Debug + std::fmt::Display,
T: Into<ShortcutOwned>,
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
}
+14 -14
View File
@@ -43,8 +43,12 @@ pub mod ui_images {
} }
} }
pub fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> { pub fn load_image_from_path(
let image = image::io::Reader::open(path)?.with_guessed_format()?.decode()?; path: &std::path::Path,
) -> Result<egui::ColorImage, image::ImageError> {
let image = image::io::Reader::open(path)?
.with_guessed_format()?
.decode()?;
let size = [image.width() as _, image.height() as _]; let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8(); let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples(); let pixels = image_buffer.as_flat_samples();
@@ -53,18 +57,14 @@ pub mod ui_images {
pixels.as_slice(), pixels.as_slice(),
)) ))
} }
pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> { pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?; let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _]; let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8(); let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples(); let pixels = image_buffer.as_flat_samples();
Ok(ColorImage::from_rgba_unmultiplied( Ok(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()))
size,
pixels.as_slice(),
))
} }
} }
#[cfg(test)] #[cfg(test)]
@@ -95,9 +95,9 @@ mod tests {
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] // #[test]
pub fn test_image_load_animated_webp2() { // pub fn test_image_load_animated_webp2() {
let res = load_image_from_path(std::path::Path::new("src/testdata/tunic.webp")); // let res = load_image_from_path(std::path::Path::new("src/testdata/tunic.webp"));
assert!(res.is_err()); // assert!(res.is_err());
} // }
} }
+2 -7
View File
@@ -57,13 +57,8 @@ impl MyEguiApp {
ui.heading(&user.path.to_string_lossy().to_string()); ui.heading(&user.path.to_string_lossy().to_string());
} }
for shortcut in user.shortcuts.iter() { for shortcut in user.shortcuts.iter() {
if shortcut.is_boilr_shortcut() if shortcut.is_boilr_shortcut() && ui.button(&shortcut.app_name).clicked() && disconnect_shortcut(&self.settings, shortcut.app_id).is_ok() {
&& ui.button(&shortcut.app_name).clicked() redraw = shortcut.app_id;
{
if disconnect_shortcut(&self.settings, shortcut.app_id).is_ok()
{
redraw = shortcut.app_id;
}
} }
} }
} }
-1
View File
@@ -353,7 +353,6 @@ impl MyEguiApp {
} }
pub(crate) fn render_ui_images(&mut self, ui: &mut egui::Ui) { pub(crate) fn render_ui_images(&mut self, ui: &mut egui::Ui) {
self.ensure_games_loaded();
self.ensure_steam_users_loaded(); self.ensure_steam_users_loaded();
if let Some(error_message) = &self.image_selected_state.settings_error { if let Some(error_message) = &self.image_selected_state.settings_error {
+135 -103
View File
@@ -2,15 +2,18 @@ use eframe::egui;
use egui::ScrollArea; use egui::ScrollArea;
use futures::executor::block_on; use futures::executor::block_on;
use steam_shortcuts_util::shortcut::ShortcutOwned;
use tokio::sync::watch; use tokio::sync::watch;
use crate::config::get_renames_file; 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;
use crate::sync::{download_images, SyncProgress}; use crate::sync::{download_images, SyncProgress};
use super::{backup_shortcuts, ImageSelectState}; use super::{backup_shortcuts, all_ready, get_all_games};
use super::{ use super::{
ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR},
MyEguiApp, MyEguiApp,
@@ -32,22 +35,12 @@ impl<T> FetcStatus<T> {
FetcStatus::Fetched(_) => true, FetcStatus::Fetched(_) => true,
} }
} }
pub fn needs_fetching(&self) -> bool {
match self {
FetcStatus::NeedsFetched => true,
FetcStatus::Fetching => false,
FetcStatus::Fetched(_) => false,
}
}
} }
impl MyEguiApp { impl MyEguiApp {
pub(crate) fn render_import_games(&mut self, ui: &mut egui::Ui) { pub(crate) fn render_import_games(&mut self, ui: &mut egui::Ui) {
ui.heading("Import Games"); ui.heading("Import Games");
self.ensure_games_loaded();
let mut scroll_style = ui.style_mut(); let mut scroll_style = ui.style_mut();
scroll_style.visuals.extreme_bg_color = BACKGROUND_COLOR; scroll_style.visuals.extreme_bg_color = BACKGROUND_COLOR;
scroll_style.visuals.widgets.inactive.bg_fill = EXTRA_BACKGROUND_COLOR; scroll_style.visuals.widgets.inactive.bg_fill = EXTRA_BACKGROUND_COLOR;
@@ -60,83 +53,79 @@ impl MyEguiApp {
.auto_shrink([false,true]) .auto_shrink([false,true])
.show(ui,|ui| { .show(ui,|ui| {
ui.reset_style(); ui.reset_style();
ui.label("Select the games you want to import into steam");
let borrowed_games = &*self.games_to_sync.borrow(); for (name,status) in &self.games_to_sync{
match borrowed_games{ ui.heading(name);
FetcStatus::Fetched(games_to_sync) => { match &*status.borrow(){
ui.label("Select the games you want to import into steam"); FetcStatus::NeedsFetched => {ui.label("Need to find games");},
for (platform_name, shortcuts) in games_to_sync{ FetcStatus::Fetching => {
ui.heading(platform_name); ui.horizontal(|ui|{
ui.spinner();
for shortcut in shortcuts { ui.label("Finding installed games");
let mut import_game = !self.settings.blacklisted_games.contains(&shortcut.app_id); });
ui.horizontal(|ui|{ },
if self.current_edit == Option::Some(shortcut.app_id){ FetcStatus::Fetched(shortcuts) => {
if let Some(new_name) = self.rename_map.get_mut(&shortcut.app_id){ match shortcuts{
ui.text_edit_singleline(new_name).request_focus(); Ok(shortcuts) => {
if ui.button("Rename").clicked() { if shortcuts.is_empty(){
if new_name.is_empty(){ ui.label("Did not find any games");
*new_name = shortcut.app_name.to_string(); }
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; } else {
let rename_file_path = get_renames_file(); let name = self.rename_map.get(&shortcut.app_id).unwrap_or(&shortcut.app_name);
let contents = serde_json::to_string(&self.rename_map); let checkbox = egui::Checkbox::new(&mut import_game,name);
if let Ok(contents) = contents{ let response = ui.add(checkbox);
let res = std::fs::write(&rename_file_path, contents); if response.double_clicked(){
println!("Write rename file at {:?} with result: {:?}",rename_file_path, res); 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){ Err(err) => {
self.settings.blacklisted_games.push(shortcut.app_id); ui.label("Failed finding games").on_hover_text(format!("Error message: {err}"));
}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");
});
},
}; };
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 ) { pub fn run_sync(&mut self, wait: bool ) {
let (sender, reciever) = watch::channel(SyncProgress::NotStarted); let (sender, reciever) = watch::channel(SyncProgress::NotStarted);
let settings = self.settings.clone(); let settings = self.settings.clone();
@@ -144,34 +133,77 @@ impl MyEguiApp {
crate::steam::ensure_steam_stopped(); crate::steam::ensure_steam_stopped();
} }
//TODO This might break cli sync, test it
self.status_reciever = reciever; self.status_reciever = reciever;
let renames = self.rename_map.clone(); let renames = self.rename_map.clone();
let handle = self.rt.spawn_blocking(move || { let all_ready= all_ready(&self.games_to_sync);
MyEguiApp::save_settings_to_file(&settings); let _ = sender.send(SyncProgress::Starting);
let mut some_sender = Some(sender); if all_ready{
backup_shortcuts(&settings.steam); let shortcuts_to_import = get_all_games(&self.games_to_sync);
let usersinfo = sync::run_sync(&settings, &mut some_sender,&renames).unwrap(); let handle = self.rt.spawn_blocking(move || {
let task = download_images(&settings, &usersinfo, &mut some_sender);
block_on(task);
//Run a second time to fix up shortcuts after images are downloaded #[cfg(target_family = "unix")]
sync::run_sync(&settings, &mut some_sender,&renames).unwrap(); setup_proton(shortcuts_to_import.iter());
if let Some(sender) = some_sender { let import_games = to_shortcut_owned(shortcuts_to_import);
let _ = sender.send(SyncProgress::Done);
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<ShortcutToImport>)>) -> Vec<(String, Vec<ShortcutOwned>)> {
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<Item = &'a (String,Vec<ShortcutToImport>)>
{
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);
}
}
+4 -263
View File
@@ -2,10 +2,6 @@ use copypasta::ClipboardProvider;
use eframe::egui; use eframe::egui;
use egui::ScrollArea; use egui::ScrollArea;
use crate::{egs::EpicPlatform};
#[cfg(target_family = "unix")]
use crate::heroic::HeroicPlatform;
use super::{ use super::{
ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR},
MyEguiApp, MyEguiApp,
@@ -34,206 +30,15 @@ impl MyEguiApp {
self.render_steam_settings(ui); self.render_steam_settings(ui);
self.render_epic_settings(ui); for platform in &mut self.platforms{
platform.render_ui(ui);
#[cfg(target_family = "unix")] ui.add_space(SECTION_SPACING);
{
self.render_heroic_settings(ui);
self.render_bottles_settings(ui);
} }
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)); 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) { fn render_steam_settings(&mut self, ui: &mut egui::Ui) {
ui.heading("Steam"); ui.heading("Steam");
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -317,68 +122,4 @@ self.settings.heroic.default_launch_through_heroic{
} }
ui.add_space(SECTION_SPACING); 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);
}
}
} }
+79 -30
View File
@@ -1,17 +1,18 @@
use std::{collections::HashMap, error::Error}; use std::{collections::HashMap, error::Error, time::Duration};
#[cfg(target_family = "unix")]
use crate::heroic::HeroicGameType;
use eframe::{egui, App, Frame}; use eframe::{egui, App, Frame};
use egui::{ImageButton, Rounding, Stroke, TextureHandle}; use egui::{ImageButton, Rounding, Stroke, TextureHandle};
use steam_shortcuts_util::shortcut::ShortcutOwned;
use tokio::{ use tokio::{
runtime::Runtime, runtime::Runtime,
sync::watch::{self, Receiver}, 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::{ use super::{
ui_colors::{ ui_colors::{
@@ -31,58 +32,78 @@ struct UiImages {
save_button: Option<egui::TextureHandle>, save_button: Option<egui::TextureHandle>,
logo_32: Option<egui::TextureHandle>, logo_32: Option<egui::TextureHandle>,
} }
type GamesToSync = Vec<(
String,
Receiver<FetcStatus<eyre::Result<Vec<ShortcutToImport>>>>,
)>;
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<ShortcutToImport>)> {
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 { pub struct MyEguiApp {
selected_menu: Menues, selected_menu: Menues,
pub(crate) settings: Settings, pub(crate) settings: Settings,
pub(crate) rt: Runtime, pub(crate) rt: Runtime,
ui_images: UiImages, ui_images: UiImages,
pub(crate) games_to_sync: Receiver<FetcStatus<Vec<(String, Vec<ShortcutOwned>)>>>, pub(crate) games_to_sync: GamesToSync,
pub(crate) status_reciever: Receiver<SyncProgress>, pub(crate) status_reciever: Receiver<SyncProgress>,
pub(crate) epic_manifests: Option<Vec<ManifestItem>>,
#[cfg(target_family = "unix")]
pub(crate) heroic_games: Option<Vec<HeroicGameType>>,
pub(crate) image_selected_state: ImageSelectState, pub(crate) image_selected_state: ImageSelectState,
pub(crate) backup_state: BackupState, pub(crate) backup_state: BackupState,
pub(crate) disconect_state: DiconnectState, pub(crate) disconect_state: DiconnectState,
pub(crate) rename_map : HashMap<u32,String>, pub(crate) rename_map: HashMap<u32, String>,
pub(crate) current_edit : Option<u32>, pub(crate) current_edit: Option<u32>,
pub(crate) platforms: Platforms,
} }
impl MyEguiApp { impl MyEguiApp {
pub fn new() -> Self { 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 { Self {
selected_menu: Menues::Import, selected_menu: Menues::Import,
settings: Settings::new().expect("We must be able to load our settings"), settings,
rt: runtime, rt: runtime,
games_to_sync: watch::channel(FetcStatus::NeedsFetched).1, games_to_sync,
ui_images: UiImages::default(), ui_images: UiImages::default(),
status_reciever: watch::channel(SyncProgress::NotStarted).1, status_reciever: watch::channel(SyncProgress::NotStarted).1,
epic_manifests: None,
#[cfg(target_family = "unix")]
heroic_games: None,
image_selected_state: ImageSelectState::default(), image_selected_state: ImageSelectState::default(),
backup_state: BackupState::default(), backup_state: BackupState::default(),
disconect_state: DiconnectState::default(), disconect_state: DiconnectState::default(),
rename_map: get_rename_map(), rename_map: get_rename_map(),
current_edit: Option::None current_edit: Option::None,
platforms,
} }
} }
} }
fn get_rename_map() -> HashMap<u32,String>{ fn get_rename_map() -> HashMap<u32, String> {
try_get_rename_map().unwrap_or_default() try_get_rename_map().unwrap_or_default()
} }
fn try_get_rename_map() -> Result<HashMap<u32,String>,Box<dyn Error>>{ fn try_get_rename_map() -> Result<HashMap<u32, String>, Box<dyn Error>> {
let rename_map = get_renames_file(); let rename_map = get_renames_file();
let file_content = std::fs::read_to_string(rename_map)?; let file_content = std::fs::read_to_string(rename_map)?;
let deserialized = serde_json::from_str(&file_content)?; let deserialized = serde_json::from_str(&file_content)?;
Ok(deserialized) Ok(deserialized)
} }
#[derive(PartialEq)] #[derive(PartialEq, Clone)]
enum Menues { enum Menues {
Import, Import,
Settings, Settings,
@@ -97,6 +118,23 @@ impl Default for Menues {
} }
} }
fn create_games_to_sync(rt: &mut Runtime, platforms: &[Box<dyn GamesPlatform>]) -> 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 { impl App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
let frame = egui::Frame::default() let frame = egui::Frame::default()
@@ -111,6 +149,8 @@ impl App for MyEguiApp {
ui.image(texture, size); ui.image(texture, size);
ui.add_space(SECTION_SPACING); ui.add_space(SECTION_SPACING);
let menu_before = self.selected_menu.clone();
let mut changed = ui let mut changed = ui
.selectable_value(&mut self.selected_menu, Menues::Import, "Import Games") .selectable_value(&mut self.selected_menu, Menues::Import, "Import Games")
.changed(); .changed();
@@ -138,9 +178,12 @@ impl App for MyEguiApp {
if changed { if changed {
self.backup_state.available_backups = None; 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 //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); let save_button = ImageButton::new(texture, size * 0.5);
if ui.add(save_button).on_hover_text("Save settings").clicked() { 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") egui::TopBottomPanel::new(egui::panel::TopBottomSide::Bottom, "Bottom Panel")
.frame(frame) .frame(frame)
.show(ctx, |ui| { .show(ctx, |ui| {
@@ -186,14 +229,16 @@ impl App for MyEguiApp {
ui.label(&status_string); ui.label(&status_string);
} }
} }
let all_ready = all_ready(&self.games_to_sync);
let texture = self.get_import_image(ui); let texture = self.get_import_image(ui);
let size = texture.size_vec2(); let size = texture.size_vec2();
let image_button = ImageButton::new(texture, size * 0.5); let image_button = ImageButton::new(texture, size * 0.5);
if ui if all_ready
.add(image_button) && ui
.on_hover_text("Import your games into steam") .add(image_button)
.clicked() .on_hover_text("Import your games into steam")
.clicked()
&& !syncing && !syncing
{ {
self.run_sync(false); self.run_sync(false);
@@ -293,6 +338,10 @@ fn setup(ctx: &egui::Context) {
} }
pub fn run_sync() { pub fn run_sync() {
let mut app = MyEguiApp::new(); 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); app.run_sync(true);
} }
-6
View File
@@ -1,6 +0,0 @@
mod game;
mod platform;
mod settings;
pub use platform::Uplay;
pub use settings::UplaySettings;
-138
View File
@@ -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<Game, Box<dyn Error>> 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<Vec<Game>, Box<dyn Error>> {
#[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<PathBuf> {
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<String, _> = 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<Vec<Game>, Box<dyn Error>> {
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<String, _> = 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<String, _> = 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)
}
-6
View File
@@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UplaySettings {
pub enabled: bool,
}