From a7524400f3a18566a4bf6d575c516d5042a2eb32 Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Sat, 26 Mar 2022 21:51:56 +0100 Subject: [PATCH] Steam collections (#60) --- Cargo.lock | 146 +++++- Cargo.toml | 4 +- gui/mainview.fl | 11 +- src/defaultconfig.toml | 3 +- src/steam/collections.rs | 422 ++++++++++++++++++ src/steam/mod.rs | 2 + src/steam/settings.rs | 1 + src/steam/utils.rs | 4 + src/sync/synchronization.rs | 123 ++--- src/testdata/leveldb/test_collection.json | 1 + .../leveldb/test_collection_value.json | 1 + src/testdata/leveldb/testcollections.json | 1 + .../leveldb/testcollections_formattet.json | 271 +++++++++++ src/testdata/vdf/user-collections.json | 32 ++ src/ui/ui.rs | 4 + 15 files changed, 962 insertions(+), 64 deletions(-) create mode 100644 src/steam/collections.rs create mode 100644 src/testdata/leveldb/test_collection.json create mode 100644 src/testdata/leveldb/test_collection_value.json create mode 100644 src/testdata/leveldb/testcollections.json create mode 100644 src/testdata/leveldb/testcollections_formattet.json create mode 100644 src/testdata/vdf/user-collections.json diff --git a/Cargo.lock b/Cargo.lock index ea76651..01cc35a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,7 @@ dependencies = [ name = "boilr" version = "0.2.7" dependencies = [ + "base64", "config", "dashmap", "failure", @@ -99,6 +100,7 @@ dependencies = [ "nom 7.0.0", "nom_locate", "reqwest", + "rusty-leveldb", "serde 1.0.130", "serde_json", "steam_shortcuts_util", @@ -108,6 +110,12 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "build_const" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" + [[package]] name = "bumpalo" version = "3.7.1" @@ -197,6 +205,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -246,6 +263,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "failure" version = "0.1.8" @@ -351,6 +389,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "funty" version = "1.1.0" @@ -451,6 +499,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -594,6 +653,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "integer-encoding" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dc51180a9b377fd75814d0cc02199c20f8e99433d6762f650d39cdbbd3b56f" + [[package]] name = "ipnet" version = "2.3.1" @@ -970,6 +1035,19 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + [[package]] name = "rand" version = "0.8.4" @@ -977,9 +1055,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -989,7 +1077,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -998,7 +1095,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom", + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -1007,7 +1113,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -1092,6 +1198,20 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rusty-leveldb" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19365c4619fa0892aef573e6ee1b569b1161ac1ac3da765f550a17645192615e" +dependencies = [ + "crc", + "errno", + "fs2", + "integer-encoding", + "rand 0.7.3", + "snap", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1219,6 +1339,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +[[package]] +name = "snap" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451" + [[package]] name = "socket2" version = "0.4.4" @@ -1296,7 +1422,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if", "libc", - "rand", + "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -1480,6 +1606,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 919ca2f..c9a53d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ futures = { version = "^0.3.17" } dashmap = { version = "^4.0.2", features = ["serde"] } fltk-theme = {version ="0.4" , optional = true } is_executable = "1.0.1" +rusty-leveldb = "0.3.6" +base64="0.13.0" [build-dependencies] fl2rust = { version = "0.4", optional = true } @@ -33,4 +35,4 @@ ui = ["fltk", "fl2rust", "toml", "fltk-theme"] [target.'cfg(windows)'.dependencies] -winreg = "0.10.1" \ No newline at end of file +winreg = "0.10.1" diff --git a/gui/mainview.fl b/gui/mainview.fl index 6fccf90..636ad47 100644 --- a/gui/mainview.fl +++ b/gui/mainview.fl @@ -12,11 +12,16 @@ class UserInterface {open } { # ----- Steam ----- - Fl_Input steam_location_input { - label {Steam Location} - xywh {25 25 450 25} align 5 + Fl_Check_Button steam_create_collections { + label {Create Collections} + tooltip {Create Collections for each platform} + xywh {25 25 155 25} down_box DOWN_BOX } + Fl_Input steam_location_input { + label {Steam Location} + xywh {175 25 300 25} align 5 + } # ----- Art download ----- Fl_Check_Button enable_steamgrid_db_checkbox { diff --git a/src/defaultconfig.toml b/src/defaultconfig.toml index af119b8..809d34f 100644 --- a/src/defaultconfig.toml +++ b/src/defaultconfig.toml @@ -32,4 +32,5 @@ enabled = true [heroic] enabled = true -[steam] \ No newline at end of file +[steam] +create_collections = false \ No newline at end of file diff --git a/src/steam/collections.rs b/src/steam/collections.rs new file mode 100644 index 0000000..0131c7c --- /dev/null +++ b/src/steam/collections.rs @@ -0,0 +1,422 @@ +use nom::FindSubstring; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use rusty_leveldb::{LdbIterator, Options, WriteBatch, DB}; + +const BOILR_TAG: &'static str = "boilr"; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +enum SteamCollection { + Actual(ActualSteamCollection), + Deleted(DeletedCollection), +} + +impl SteamCollection { + pub fn is_boilr_collection(&self) -> bool { + match self { + SteamCollection::Actual(actual) => actual.is_boilr_collection(), + SteamCollection::Deleted(_) => false, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct ActualSteamCollection { + key: String, + timestamp: u64, + value: String, //For custom collections, this value is a ValueCollection json serialized + #[serde( + skip_serializing_if = "Option::is_none", + rename = "conflictResolutionMethod" + )] + conflict_resolution_method: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "strMethodId")] + str_method_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, +} + +impl ActualSteamCollection { + fn new>(name: B, ids: &Vec) -> Self { + let name = name.as_ref(); + let key = format!("user-collections.{}", name_to_key(name)); + let value = serialize_collection_value(name, ids); + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let timestamp = since_the_epoch.as_secs(); + + ActualSteamCollection { + key, + timestamp, + value, + conflict_resolution_method: Some("custom".to_string()), + str_method_id: Some("union-collections".to_string()), + version: None, + } + } + + pub fn is_boilr_collection(&self) -> bool { + self.key + .contains(&format!("user-collections.{}", BOILR_TAG)) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct ValueCollection { + id: String, + name: String, + added: Vec, + removed: Vec, +} + +impl ValueCollection { + fn new>(name: S, game_ids: &Vec) -> Self { + let name = name.as_ref(); + let id = name_to_key(name); + let value = ValueCollection { + id, + name: name.to_string(), + added: game_ids.clone(), + removed: vec![], + }; + value + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] + +struct DeletedCollection { + key: String, + timestamp: usize, + is_deleted: bool, + version: String, +} + +pub struct Collection { + pub name: String, + pub game_ids: Vec, +} + +pub fn write_collections>( + steam_user_id: S, + collections_to_add: &Vec, +) -> Result<(), Box> { + let steam_user_id = steam_user_id.as_ref(); + let new_collections: Vec<(String, SteamCollection)> = collections_to_add + .iter() + .map(|c| { + let collection = ActualSteamCollection::new(&c.name, &c.game_ids); + (collection.key.clone(), SteamCollection::Actual(collection)) + }) + .collect(); + + let mut db = open_db()?; + + let current_categories = get_categories(steam_user_id, &mut db)?; + //this is a collection of collections, known as a category + let mut write_batch = WriteBatch::new(); + + for (category_key, mut collections) in current_categories { + collections.retain(|(_key, collection)| !collection.is_boilr_collection()); + collections.extend(new_collections.clone()); + save_category(category_key, collections, &mut write_batch)?; + + if let Some(path) = get_vdf_path(steam_user_id) { + let content = std::fs::read_to_string(&path).expect("Should be able to read this file"); + if let Some(mut vdf_collections) = parse_vdf_collection(content) { + let boilr_keys: Vec = vdf_collections + .keys() + .filter(|k| k.contains(BOILR_TAG)) + .map(|k| k.clone()) + .collect(); + for key in boilr_keys { + vdf_collections.remove(&key); + } + + let new_vdfs = collections_to_add.iter().map(|collection| { + let key = name_to_key(&collection.name); + let vd_collection = VdfCollection { + id: key, + added: collection.game_ids.clone(), + removed: vec![], + }; + vd_collection + }); + for new_vdf in new_vdfs { + vdf_collections.insert(new_vdf.id.clone(), new_vdf.clone()); + } + + let new_string = write_vdf_collection_to_string( + &path.clone().to_string_lossy(), + &vdf_collections, + ); + if let Some(new_string) = new_string { + std::fs::write(path, new_string).unwrap(); + } + } + } + } + + db.write(write_batch, true)?; + + Ok(()) +} + +#[cfg(target_family = "unix")] +fn get_vdf_path>(steamid: S) -> Option { + match std::env::var("HOME") { + Ok(home) => { + let path = Path::new(&home) + .join(".steam") + .join("steam") + .join("userdata") + .join(steamid.as_ref()) + .join("config") + .join("localconfig.vdf"); + if path.exists() { + return Some(path.to_path_buf()); + } + return None; + } + Err(e) => return None, + } +} + +#[cfg(target_os = "windows")] +fn get_vdf_path>(steamid: S) -> Option { + match std::env::var("PROGRAMFILES(X86)") { + Ok(program_files) => { + let path = Path::new(&program_files) + .join("Steam") + .join("userdata") + .join(steamid.as_ref()) + .join("config") + .join("localconfig.vdf"); + if path.exists() { + Some(path.to_path_buf()) + } else { + None + } + } + Err(_e) => None, + } +} + +fn save_category>( + category_key: S, + category: Vec<(String, SteamCollection)>, + batch: &mut WriteBatch, +) -> Result<(), Box> { + let json = serde_json::to_string(&category)?; + let prefixed = format!("\u{1}{}", json); + batch.put(category_key.as_ref().as_bytes(), prefixed.as_bytes()); + Ok(()) +} + +fn get_categories>( + steamid: S, + db: &mut DB, +) -> Result>, Box> { + let namespace_keys = get_namespace_keys(steamid, db); + let mut db_iter = db.new_iter()?; + let mut res = HashMap::new(); + while let Some((key_bytes, data_bytes)) = db_iter.next() { + let key = String::from_utf8_lossy(&key_bytes).to_string(); + //make sure that what we are looking at is a collection + //there are other things in this db as well + if namespace_keys.contains(&key) { + let data = String::from_utf8_lossy(&data_bytes); + let collections = parse_steam_collections(&data)?; + res.insert(key, collections); + } + } + Ok(res) +} + +fn open_db() -> Result> { + let location = get_level_db_location(); + let options = Options::default(); + let open_res = DB::open(location.unwrap(), options); + if let Err(e) = &open_res { + match &e.code { + rusty_leveldb::StatusCode::LockError => { + println!("Could not lock the steam level database, make sure steam is turned off when running synchronizations"); + } + rusty_leveldb::StatusCode::NotFound => { + println!("Could not find the steam level database, try to open and close steam once and synchronize again"); + } + _ => {} + }; + } + Ok(open_res?) +} + +fn get_namespace_keys>(steamid: S, db: &mut DB) -> HashSet { + let keyprefix = get_steam_user_prefix(steamid); + + let namespaces_key = format!("{}s", keyprefix); + let key_bytes = namespaces_key.as_bytes(); + let namespaces = get_namespaces(db, key_bytes).unwrap_or_default(); + let namespace_keys = namespaces + .iter() + .map(|c| format!("{}-{}", keyprefix, c.0)) + .collect(); + namespace_keys +} + +fn get_namespaces(db: &mut DB, key_bytes: &[u8]) -> Option> { + match db.get(key_bytes) { + Some(got) => { + let collection_bytes = got.as_slice(); + let collectin_str = String::from_utf8_lossy(&collection_bytes)[1..].to_string(); + let collection = serde_json::from_str(&collectin_str).unwrap_or_default(); + Some(collection) + } + _ => None, + } +} + +#[cfg(target_family = "unix")] +fn get_level_db_location() -> Option { + match std::env::var("HOME") { + Ok(home) => { + let path = Path::new(&home) + .join(".steam") + .join("steam") + .join("config") + .join("htmlcache") + .join("Local Storage") + .join("leveldb"); + if path.exists() { + return Some(path.to_path_buf()); + } + return None; + } + Err(e) => return None, + } +} + +#[cfg(target_os = "windows")] +fn get_level_db_location() -> Option { + match std::env::var("LOCALAPPDATA") { + Ok(localdata) => { + let path = Path::new(&localdata) + .join("Steam") + .join("htmlcache") + .join("Local Storage") + .join("leveldb"); + if path.exists() { + return Some(path.to_path_buf()); + } + return None; + } + Err(_e) => return None, + } +} + +fn serialize_collection_value>(name: S, game_ids: &Vec) -> String { + let value = ValueCollection::new(name, game_ids); + let value_json = serde_json::to_string(&value).expect("Should be able to serialize known type"); + value_json +} + +fn name_to_key>(name: S) -> String { + let config = base64::Config::new(base64::CharacterSet::Standard, false); + let base64 = base64::encode_config(name.as_ref(), config); + let key = format!("{}-{}", BOILR_TAG, base64); + key +} + +fn parse_steam_collections>( + input: S, +) -> Result, Box> { + let input = input.as_ref(); + let input = if input.starts_with("\u{1}") { + input[1..].to_string() + } else { + input.to_string() + }; + let res = serde_json::from_str::>(&input)?; + Ok(res) +} + +fn get_steam_user_prefix>(steamid: S) -> String { + let keyprefix = format!( + "_https://steamloopback.host\u{0000}\u{0001}U{}-cloud-storage-namespace", + steamid.as_ref() + ); + keyprefix +} + +pub fn parse_vdf_collection>(input: S) -> Option> { + let input = input.as_ref(); + serde_json::from_str(input).ok() +} + +pub fn write_vdf_collection_to_string>( + input: S, + vdf: &HashMap, +) -> Option { + let input = input.as_ref(); + let str = serde_json::to_string(vdf).expect("Should be able to serialize known type"); + let encoded_json = format!("\"{}\"", str.replace("\"", "\\\"")); + let key = "\t\"user-collections\"\t\t"; + if let Some(start_index) = input.find_substring(key) { + let start_index_plus_key = start_index + key.len(); + if let Some(line_index) = input[start_index_plus_key..].find("\n") { + let end_index_in_full = line_index + start_index_plus_key; + let result = format!( + "{}{}{}", + input[..start_index_plus_key].to_string(), + encoded_json, + input[end_index_in_full..].to_string() + ); + return Some(result); + } + } + None +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VdfCollection { + id: String, + added: Vec, + removed: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_serialize_collection() { + let games = vec![312200]; + let mut collection = ActualSteamCollection::new("Itch", &games); + collection.timestamp = 1647763452; + let json = serde_json::to_string(&collection).unwrap(); + let expected = include_str!("../testdata/leveldb/test_collection.json"); + assert_eq!(json, expected); + } + + #[test] + fn can_serialize_collection_inner() { + let games = vec![312200]; + let res = serialize_collection_value("Itch", &games); + let expected = include_str!("../testdata/leveldb/test_collection_value.json"); + assert_eq!(res, expected); + } + + #[test] + fn can_parse_categories() { + let input = include_str!("../testdata/leveldb/testcollections.json"); + let collection = parse_steam_collections(input).unwrap(); + assert_eq!(28, collection.len()) + } +} diff --git a/src/steam/mod.rs b/src/steam/mod.rs index e27ad7c..cdb3496 100644 --- a/src/steam/mod.rs +++ b/src/steam/mod.rs @@ -1,5 +1,7 @@ mod settings; mod utils; +mod collections; pub use settings::SteamSettings; pub use utils::*; +pub use collections::*; diff --git a/src/steam/settings.rs b/src/steam/settings.rs index 01efeba..6eedcc4 100644 --- a/src/steam/settings.rs +++ b/src/steam/settings.rs @@ -3,4 +3,5 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SteamSettings { pub location: Option, + pub create_collections: bool, } diff --git a/src/steam/utils.rs b/src/steam/utils.rs index d2f7a5f..05aabc4 100644 --- a/src/steam/utils.rs +++ b/src/steam/utils.rs @@ -46,6 +46,7 @@ pub struct ShortcutInfo { pub struct SteamUsersInfo { pub steam_user_data_folder: String, pub shortcut_path: Option, + pub user_id : String, } /// Get the paths to the steam users shortcuts (one for each user) @@ -80,6 +81,7 @@ pub fn get_shortcuts_paths( _ => false, }) .map(|folder| { + let user_id = folder.file_name().to_string_lossy().to_string(); let folder_path = folder.path(); let folder_str = folder_path .to_str() @@ -91,11 +93,13 @@ pub fn get_shortcuts_paths( return SteamUsersInfo { steam_user_data_folder: folder_string, shortcut_path: Some(shortcuts_path.to_str().unwrap().to_string()), + user_id }; } else { return SteamUsersInfo { steam_user_data_folder: folder_string, shortcut_path: None, + user_id }; } }) diff --git a/src/sync/synchronization.rs b/src/sync/synchronization.rs index 8a05769..f58241e 100644 --- a/src/sync/synchronization.rs +++ b/src/sync/synchronization.rs @@ -2,13 +2,17 @@ use steam_shortcuts_util::{shortcut::ShortcutOwned, shortcuts_to_bytes}; use crate::{ egs::EpicPlatform, + heroic::HeroicPlatform, legendary::LegendaryPlatform, lutris::lutris_platform::LutrisPlatform, platform::Platform, settings::Settings, - steam::{get_shortcuts_for_user, get_shortcuts_paths, ShortcutInfo, SteamUsersInfo}, + steam::{ + get_shortcuts_for_user, get_shortcuts_paths, write_collections, Collection, ShortcutInfo, + SteamUsersInfo, + }, steamgriddb::download_images_for_users, - uplay::Uplay, heroic::HeroicPlatform, + uplay::Uplay, }; use std::error::Error; @@ -19,6 +23,12 @@ const BOILR_TAG: &str = "boilr"; pub async fn run_sync(settings: &Settings) -> Result<(), Box> { let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam)?; + + let platform_shortcuts = get_platform_shortcuts(settings); + let all_shortcuts: Vec = platform_shortcuts + .iter() + .flat_map(|s| s.1.clone()) + .collect(); println!("Found {} user(s)", userinfo_shortcuts.len()); for user in userinfo_shortcuts.iter_mut() { let start_time = std::time::Instant::now(); @@ -27,14 +37,22 @@ pub async fn run_sync(settings: &Settings) -> Result<(), Box> { println!( "Found {} shortcuts for user: {}", shortcut_info.shortcuts.len(), - user.steam_user_data_folder + user.user_id ); remove_old_shortcuts(&mut shortcut_info); - update_platforms(settings, &mut shortcut_info.shortcuts); + + shortcut_info.shortcuts.extend(all_shortcuts.clone()); + fix_shortcut_icons(user, &mut shortcut_info.shortcuts); save_shortcuts(&shortcut_info.shortcuts, Path::new(&shortcut_info.path)); - user.shortcut_path = Some(shortcut_info.path.to_string_lossy().to_string()); + + if settings.steam.create_collections { + match write_shortcut_collections(&user.user_id, &platform_shortcuts) { + Ok(_) => (), + Err(_e) => eprintln!("Could not write collections, make sure steam is shut down"), + } + } let duration = start_time.elapsed(); println!("Finished synchronizing games in: {:?}", duration); @@ -84,50 +102,46 @@ fn fix_shortcut_icons(user: &SteamUsersInfo, shortcuts: &mut Vec) } } -fn update_platforms(settings: &Settings, new_user_shortcuts: &mut Vec) { - update_platform_shortcuts( - &EpicPlatform::new(settings.epic_games.clone()), - new_user_shortcuts, - ); - update_platform_shortcuts( - &LegendaryPlatform::new(settings.legendary.clone()), - new_user_shortcuts, - ); - update_platform_shortcuts( - &ItchPlatform::new(settings.itch.clone()), - new_user_shortcuts, - ); - update_platform_shortcuts( - &OriginPlatform { - settings: settings.origin.clone(), - }, - new_user_shortcuts, - ); - update_platform_shortcuts( - &GogPlatform { - settings: settings.gog.clone(), - }, - new_user_shortcuts, - ); - update_platform_shortcuts( - &Uplay { - settings: settings.uplay.clone(), - }, - new_user_shortcuts, - ); +fn write_shortcut_collections>( + steam_id: S, + platform_results: &Vec<(String, Vec)>, +) -> Result<(), Box> { + let mut collections = vec![]; - update_platform_shortcuts( - &LutrisPlatform { + for (name, shortcuts) in platform_results { + let game_ids = shortcuts.iter().map(|s| (s.app_id as usize)).collect(); + collections.push(Collection { + name: name.clone(), + game_ids, + }); + } + println!("Writing {} collections ", collections.len()); + write_collections(steam_id.as_ref(), &collections)?; + Ok(()) +} + +fn get_platform_shortcuts(settings: &Settings) -> Vec<(String, Vec)> { + let platform_results = vec![ + update_platform_shortcuts(&EpicPlatform::new(settings.epic_games.clone())), + update_platform_shortcuts(&LegendaryPlatform::new(settings.legendary.clone())), + update_platform_shortcuts(&ItchPlatform::new(settings.itch.clone())), + update_platform_shortcuts(&OriginPlatform { + settings: settings.origin.clone(), + }), + update_platform_shortcuts(&GogPlatform { + settings: settings.gog.clone(), + }), + update_platform_shortcuts(&Uplay { + settings: settings.uplay.clone(), + }), + update_platform_shortcuts(&LutrisPlatform { settings: settings.lutris.clone(), - }, - new_user_shortcuts, - ); - update_platform_shortcuts( - &HeroicPlatform { + }), + update_platform_shortcuts(&HeroicPlatform { settings: settings.heroic.clone(), - }, - new_user_shortcuts, - ); + }), + ]; + platform_results.iter().filter_map(|p| p.clone()).collect() } fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) { @@ -157,7 +171,7 @@ fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) { } } -fn update_platform_shortcuts(platform: &P, current_shortcuts: &mut Vec) +fn update_platform_shortcuts(platform: &P) -> Option<(String, Vec)> where P: Platform, E: std::fmt::Debug + std::fmt::Display, @@ -170,9 +184,11 @@ where platform.name(), reason ); - return; + return None; } + let mut current_shortcuts = vec![]; + #[cfg(target_family = "unix")] if platform.create_symlinks() { let name = platform.name(); @@ -180,14 +196,14 @@ where } let shortcuts_to_add_result = platform.get_shortcuts(); - match shortcuts_to_add_result { Ok(shortcuts_to_add) => { let mut shortcuts_to_add_transformed = vec![]; - for shortcut in shortcuts_to_add{ + for shortcut in shortcuts_to_add { let mut shortcut_owned: ShortcutOwned = shortcut.into(); - shortcut_owned.dev_kit_game_id = format!("{}-{}",BOILR_TAG,shortcut_owned.app_id); + shortcut_owned.dev_kit_game_id = + format!("{}-{}", BOILR_TAG, shortcut_owned.app_id); shortcuts_to_add_transformed.push(shortcut_owned); } @@ -206,8 +222,10 @@ where } else { shortcut_owned }; - current_shortcuts.push(shortcut_owned); + current_shortcuts.push(shortcut_owned.clone()); } + let name = platform.name(); + return Some((name.to_string(), current_shortcuts)); } Err(err) => { eprintln!("Error getting shortcuts from platform: {}", platform.name()); @@ -215,4 +233,5 @@ where } } } + None } diff --git a/src/testdata/leveldb/test_collection.json b/src/testdata/leveldb/test_collection.json new file mode 100644 index 0000000..72b78bb --- /dev/null +++ b/src/testdata/leveldb/test_collection.json @@ -0,0 +1 @@ +{"key":"user-collections.boilr-SXRjaA","timestamp":1647763452,"value":"{\"id\":\"boilr-SXRjaA\",\"name\":\"Itch\",\"added\":[312200],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections"} \ No newline at end of file diff --git a/src/testdata/leveldb/test_collection_value.json b/src/testdata/leveldb/test_collection_value.json new file mode 100644 index 0000000..7a5e36f --- /dev/null +++ b/src/testdata/leveldb/test_collection_value.json @@ -0,0 +1 @@ +{"id":"boilr-SXRjaA","name":"Itch","added":[312200],"removed":[]} \ No newline at end of file diff --git a/src/testdata/leveldb/testcollections.json b/src/testdata/leveldb/testcollections.json new file mode 100644 index 0000000..a2ffb79 --- /dev/null +++ b/src/testdata/leveldb/testcollections.json @@ -0,0 +1 @@ +[["showcases.2",{"key":"showcases.2","timestamp":1627185068,"value":"{\"nShowcaseId\":2,\"strCollectionId\":\"type-games\",\"eSortBy\":11,\"bExpanded\":true,\"nOrder\":1572675717433,\"nLastChangedMS\":1627185068325}"}],["showcases.-1",{"key":"showcases.-1","timestamp":1572551831,"value":"{\"nShowcaseId\":-1,\"strCollectionId\":\"\",\"eSortBy\":1,\"bExpanded\":false}","conflictResolutionMethod":"initial"}],["user-collections.favorite",{"key":"user-collections.favorite","timestamp":1646915179,"value":"{\"id\":\"favorite\",\"name\":\"Favorites\",\"added\":[265930,751780,433340,361420,337340,1055540,960990,240720,1364100,398850,233290,1245620],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"766"}],["user-collections.hidden",{"key":"user-collections.hidden","timestamp":1583777127,"value":"{\"id\":\"hidden\",\"name\":\"Hidden\",\"added\":[622590,813000],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections"}],["collection-bootstrap-complete",{"key":"collection-bootstrap-complete","timestamp":1572551831,"value":"true","conflictResolutionMethod":"initial"}],["sc-version",{"key":"sc-version","timestamp":1627720308,"value":"6"}],["user-collections.uc-tyBrO3av85Yt",{"key":"user-collections.uc-tyBrO3av85Yt","timestamp":1647172018,"value":"{\"id\":\"uc-tyBrO3av85Yt\",\"name\":\"Installed\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2,1],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"767"}],["user-collections.uc-m1aOnX+0j8hd",{"key":"user-collections.uc-m1aOnX+0j8hd","timestamp":1647172018,"value":"{\"id\":\"uc-m1aOnX+0j8hd\",\"name\":\"Ready TO Play\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"767"}],["showcases.3",{"key":"showcases.3","timestamp":1589544934,"value":"{\"nShowcaseId\":3,\"strCollectionId\":\"play-next\",\"eSortBy\":1,\"bExpanded\":false,\"nOrder\":1589544935035}","conflictResolutionMethod":"initial"}],["whatsnew_lastseen",{"key":"whatsnew_lastseen","timestamp":1589544939,"value":"3"}],["user-collections.uc-Y33SC/drzUUE",{"key":"user-collections.uc-Y33SC/drzUUE","timestamp":1607528488,"value":"{\"id\":\"uc-Y33SC/drzUUE\",\"name\":\"AllGames\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}","conflictResolutionMethod":"custom","strMethodId":"union-collections"}],["user-collections.uc-in3wyYtlPtji",{"key":"user-collections.uc-in3wyYtlPtji","timestamp":1640761113,"is_deleted":true,"version":"359"}],["showcases.1616699857157",{"key":"showcases.1616699857157","timestamp":1647376978,"value":"{\"nShowcaseId\":1616699857157,\"strCollectionId\":\"recent-games\",\"eSortBy\":1,\"nLastChangedMS\":1647376978744}","version":"768"}],["user-collections.uc-viSLEvPBQo8y",{"key":"user-collections.uc-viSLEvPBQo8y","timestamp":1640761088,"is_deleted":true,"version":"354"}],["user-collections.uc-v49SJCW8hvGt",{"key":"user-collections.uc-v49SJCW8hvGt","timestamp":1640761084,"is_deleted":true,"version":"353"}],["user-collections.uc-qOFB6+htBEw+",{"key":"user-collections.uc-qOFB6+htBEw+","timestamp":1640761104,"is_deleted":true,"version":"357"}],["user-collections.uc-eK5FEQBVV/f4",{"key":"user-collections.uc-eK5FEQBVV/f4","timestamp":1640761102,"is_deleted":true,"version":"356"}],["user-collections.uc-WY8/sP0+o0mi",{"key":"user-collections.uc-WY8/sP0+o0mi","timestamp":1640761091,"is_deleted":true,"version":"355"}],["user-collections.uc-tuuA/xdtEaPL",{"key":"user-collections.uc-tuuA/xdtEaPL","timestamp":1640761110,"is_deleted":true,"version":"358"}],["user-collections.uc-JxLU0W6+VB99",{"key":"user-collections.uc-JxLU0W6+VB99","timestamp":1640778296,"is_deleted":true,"version":"513"}],["user-collections.uc-x7kKBnx8rlm1",{"key":"user-collections.uc-x7kKBnx8rlm1","timestamp":1645356629,"value":"{\"id\":\"uc-x7kKBnx8rlm1\",\"name\":\"Next\",\"added\":[1252330,403640,582160,337340,780290,236870,1182620,214560,105000,514900,480490,900270,384190,473690,34010,934780,536270,484900,984800,208650,250680,960990,692850,774361,1092430,399810,717610,893180,870780,718670,262060,214340,373650,452570,65930,330020,312200,1091500,548430,356650,17470,47780,337000,435150,373420,379720,47810,239140,761030,245280,716500,396750,470260,716490,1047220,330840,792100,368360,255070,331870,882100,268500,857980,427290,391540,499520,297130,205910,1150950,464920,2400,750920,418240,814380,302510,312520,9200,474960,741820,485030,633060,788100,760060,846470,1449560,529660,305620,1405790,414340,394510,863550,323190,233290,1349230,371140,214490,611760,1172450,206190,460950,739630,683320,304430,319630,264710,207690,1147550,601840,362890,200260,843260,557340,238320,1018850,1181400,874390,351920,274500,504210,322190,996580,813630,393190,597760,242760,470780,530320,965680,268130,424840,587430,582560,207170,960690,764790,220780,257850,234270,48000,200900,523780,1054490,428550,224480,547480,963000,613100,388880,230230,356400,475150,890720,701160,1168880,712730,493340,690640,699170,250700,522470,1127610,300550,1063490,233740,412020,291650,20900,223830,581270,281990,584980,209830,605450,1011190,555880,110800,582010,994220,115800,532210,304650,535480,443810,240760,431940,356040,231200,231910,437630],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"765"}],["user-collections.uc-BfHgIPPLf03m",{"key":"user-collections.uc-BfHgIPPLf03m","timestamp":1640778301,"is_deleted":true,"version":"514"}],["user-collections.uc-rlBhYJWJVfT7",{"key":"user-collections.uc-rlBhYJWJVfT7","timestamp":1644677712,"value":"{\"id\":\"uc-rlBhYJWJVfT7\",\"name\":\"Completed\",\"added\":[80310,269050,250320,20920,258520,237930,98100,203160,205650,257510,343860,252410,221910,646570,31220,391220,212050,227000,632470,617290,1349230,873940,294100,1102190,1092790,874260,383870,409160,861540,1222140,894020,3830,1145360,546560,28050,205100,1070710,367520,219150,220,434170,213670,420,242680,102600,1057090,221640,286160,8930,35140,246420,341800,550,261570,40700,450390,380,8870,113200,620980,107100,207610,261030,436820,4580,219890,500,204360,212910,240,35700,80350,80360,29720,49400,225080,72850,57300,26800,55230,374320,405820,858260,80340,251470,1079800,201790,248820,50300,688420,211420,283160,266510,320,448280,368230,287700,113020,1172380,462770,80330,236430,12140,322500,244160,26500,40930,400430,17410,335300,238370,631570,1151640,1055540],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"752"}],["user-collections.uc-2mg3hoGsWUvy",{"key":"user-collections.uc-2mg3hoGsWUvy","timestamp":1640778308,"is_deleted":true,"version":"515"}],["user-collections.uc-UGQi/7wUEotn",{"key":"user-collections.uc-UGQi/7wUEotn","timestamp":1644590139,"value":"{\"id\":\"uc-UGQi/7wUEotn\",\"name\":\"Single player and Story Rich\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[7],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[1742],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"749"}],["user-collections.uc-kiAlA*+Nothpn",{"key":"user-collections.uc-kiAlA*+Nothpn","timestamp":1645030805,"value":"{\"id\":\"uc-kiAlA*+Nothpn\",\"name\":\"Co-op\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[3841,1685],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"755"}],["user-collections.uc-VLUd605CHPd+",{"key":"user-collections.uc-VLUd605CHPd+","timestamp":1647763452,"value":"{\"id\":\"uc-VLUd605CHPd+\",\"name\":\"Itch\",\"added\":[312200],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"771"}],["user-collections.uc-DU8F01868yMa",{"key":"user-collections.uc-DU8F01868yMa","timestamp":1647763461,"value":"{\"id\":\"uc-DU8F01868yMa\",\"name\":\"TestTag\",\"added\":[],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections","version":"772"}]] \ No newline at end of file diff --git a/src/testdata/leveldb/testcollections_formattet.json b/src/testdata/leveldb/testcollections_formattet.json new file mode 100644 index 0000000..14a569f --- /dev/null +++ b/src/testdata/leveldb/testcollections_formattet.json @@ -0,0 +1,271 @@ +[ + [ + "showcases.2", + { + "key": "showcases.2", + "timestamp": 1627185068, + "value": "{\"nShowcaseId\":2,\"strCollectionId\":\"type-games\",\"eSortBy\":11,\"bExpanded\":true,\"nOrder\":1572675717433,\"nLastChangedMS\":1627185068325}" + } + ], + [ + "showcases.-1", + { + "key": "showcases.-1", + "timestamp": 1572551831, + "value": "{\"nShowcaseId\":-1,\"strCollectionId\":\"\",\"eSortBy\":1,\"bExpanded\":false}", + "conflictResolutionMethod": "initial" + } + ], + [ + "user-collections.favorite", + { + "key": "user-collections.favorite", + "timestamp": 1646915179, + "value": "{\"id\":\"favorite\",\"name\":\"Favorites\",\"added\":[265930,751780,433340,361420,337340,1055540,960990,240720,1364100,398850,233290,1245620],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "766" + } + ], + [ + "user-collections.hidden", + { + "key": "user-collections.hidden", + "timestamp": 1583777127, + "value": "{\"id\":\"hidden\",\"name\":\"Hidden\",\"added\":[622590,813000],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections" + } + ], + [ + "collection-bootstrap-complete", + { + "key": "collection-bootstrap-complete", + "timestamp": 1572551831, + "value": "true", + "conflictResolutionMethod": "initial" + } + ], + [ + "sc-version", + { + "key": "sc-version", + "timestamp": 1627720308, + "value": "6" + } + ], + [ + "user-collections.uc-tyBrO3av85Yt", + { + "key": "user-collections.uc-tyBrO3av85Yt", + "timestamp": 1647172018, + "value": "{\"id\":\"uc-tyBrO3av85Yt\",\"name\":\"Installed\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2,1],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "767" + } + ], + [ + "user-collections.uc-m1aOnX+0j8hd", + { + "key": "user-collections.uc-m1aOnX+0j8hd", + "timestamp": 1647172018, + "value": "{\"id\":\"uc-m1aOnX+0j8hd\",\"name\":\"Ready TO Play\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "767" + } + ], + [ + "showcases.3", + { + "key": "showcases.3", + "timestamp": 1589544934, + "value": "{\"nShowcaseId\":3,\"strCollectionId\":\"play-next\",\"eSortBy\":1,\"bExpanded\":false,\"nOrder\":1589544935035}", + "conflictResolutionMethod": "initial" + } + ], + [ + "whatsnew_lastseen", + { + "key": "whatsnew_lastseen", + "timestamp": 1589544939, + "value": "3" + } + ], + [ + "user-collections.uc-Y33SC/drzUUE", + { + "key": "user-collections.uc-Y33SC/drzUUE", + "timestamp": 1607528488, + "value": "{\"id\":\"uc-Y33SC/drzUUE\",\"name\":\"AllGames\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections" + } + ], + [ + "user-collections.uc-in3wyYtlPtji", + { + "key": "user-collections.uc-in3wyYtlPtji", + "timestamp": 1640761113, + "is_deleted": true, + "version": "359" + } + ], + [ + "showcases.1616699857157", + { + "key": "showcases.1616699857157", + "timestamp": 1647376978, + "value": "{\"nShowcaseId\":1616699857157,\"strCollectionId\":\"recent-games\",\"eSortBy\":1,\"nLastChangedMS\":1647376978744}", + "version": "768" + } + ], + [ + "user-collections.uc-viSLEvPBQo8y", + { + "key": "user-collections.uc-viSLEvPBQo8y", + "timestamp": 1640761088, + "is_deleted": true, + "version": "354" + } + ], + [ + "user-collections.uc-v49SJCW8hvGt", + { + "key": "user-collections.uc-v49SJCW8hvGt", + "timestamp": 1640761084, + "is_deleted": true, + "version": "353" + } + ], + [ + "user-collections.uc-qOFB6+htBEw+", + { + "key": "user-collections.uc-qOFB6+htBEw+", + "timestamp": 1640761104, + "is_deleted": true, + "version": "357" + } + ], + [ + "user-collections.uc-eK5FEQBVV/f4", + { + "key": "user-collections.uc-eK5FEQBVV/f4", + "timestamp": 1640761102, + "is_deleted": true, + "version": "356" + } + ], + [ + "user-collections.uc-WY8/sP0+o0mi", + { + "key": "user-collections.uc-WY8/sP0+o0mi", + "timestamp": 1640761091, + "is_deleted": true, + "version": "355" + } + ], + [ + "user-collections.uc-tuuA/xdtEaPL", + { + "key": "user-collections.uc-tuuA/xdtEaPL", + "timestamp": 1640761110, + "is_deleted": true, + "version": "358" + } + ], + [ + "user-collections.uc-JxLU0W6+VB99", + { + "key": "user-collections.uc-JxLU0W6+VB99", + "timestamp": 1640778296, + "is_deleted": true, + "version": "513" + } + ], + [ + "user-collections.uc-x7kKBnx8rlm1", + { + "key": "user-collections.uc-x7kKBnx8rlm1", + "timestamp": 1645356629, + "value": "{\"id\":\"uc-x7kKBnx8rlm1\",\"name\":\"Next\",\"added\":[1252330,403640,582160,337340,780290,236870,1182620,214560,105000,514900,480490,900270,384190,473690,34010,934780,536270,484900,984800,208650,250680,960990,692850,774361,1092430,399810,717610,893180,870780,718670,262060,214340,373650,452570,65930,330020,312200,1091500,548430,356650,17470,47780,337000,435150,373420,379720,47810,239140,761030,245280,716500,396750,470260,716490,1047220,330840,792100,368360,255070,331870,882100,268500,857980,427290,391540,499520,297130,205910,1150950,464920,2400,750920,418240,814380,302510,312520,9200,474960,741820,485030,633060,788100,760060,846470,1449560,529660,305620,1405790,414340,394510,863550,323190,233290,1349230,371140,214490,611760,1172450,206190,460950,739630,683320,304430,319630,264710,207690,1147550,601840,362890,200260,843260,557340,238320,1018850,1181400,874390,351920,274500,504210,322190,996580,813630,393190,597760,242760,470780,530320,965680,268130,424840,587430,582560,207170,960690,764790,220780,257850,234270,48000,200900,523780,1054490,428550,224480,547480,963000,613100,388880,230230,356400,475150,890720,701160,1168880,712730,493340,690640,699170,250700,522470,1127610,300550,1063490,233740,412020,291650,20900,223830,581270,281990,584980,209830,605450,1011190,555880,110800,582010,994220,115800,532210,304650,535480,443810,240760,431940,356040,231200,231910,437630],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "765" + } + ], + [ + "user-collections.uc-BfHgIPPLf03m", + { + "key": "user-collections.uc-BfHgIPPLf03m", + "timestamp": 1640778301, + "is_deleted": true, + "version": "514" + } + ], + [ + "user-collections.uc-rlBhYJWJVfT7", + { + "key": "user-collections.uc-rlBhYJWJVfT7", + "timestamp": 1644677712, + "value": "{\"id\":\"uc-rlBhYJWJVfT7\",\"name\":\"Completed\",\"added\":[80310,269050,250320,20920,258520,237930,98100,203160,205650,257510,343860,252410,221910,646570,31220,391220,212050,227000,632470,617290,1349230,873940,294100,1102190,1092790,874260,383870,409160,861540,1222140,894020,3830,1145360,546560,28050,205100,1070710,367520,219150,220,434170,213670,420,242680,102600,1057090,221640,286160,8930,35140,246420,341800,550,261570,40700,450390,380,8870,113200,620980,107100,207610,261030,436820,4580,219890,500,204360,212910,240,35700,80350,80360,29720,49400,225080,72850,57300,26800,55230,374320,405820,858260,80340,251470,1079800,201790,248820,50300,688420,211420,283160,266510,320,448280,368230,287700,113020,1172380,462770,80330,236430,12140,322500,244160,26500,40930,400430,17410,335300,238370,631570,1151640,1055540],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "752" + } + ], + [ + "user-collections.uc-2mg3hoGsWUvy", + { + "key": "user-collections.uc-2mg3hoGsWUvy", + "timestamp": 1640778308, + "is_deleted": true, + "version": "515" + } + ], + [ + "user-collections.uc-UGQi/7wUEotn", + { + "key": "user-collections.uc-UGQi/7wUEotn", + "timestamp": 1644590139, + "value": "{\"id\":\"uc-UGQi/7wUEotn\",\"name\":\"Single player and Story Rich\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[7],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[1742],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "749" + } + ], + [ + "user-collections.uc-kiAlA*+Nothpn", + { + "key": "user-collections.uc-kiAlA*+Nothpn", + "timestamp": 1645030805, + "value": "{\"id\":\"uc-kiAlA*+Nothpn\",\"name\":\"Co-op\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[3841,1685],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "755" + } + ], + [ + "user-collections.uc-VLUd605CHPd+", + { + "key": "user-collections.uc-VLUd605CHPd+", + "timestamp": 1647763452, + "value": "{\"id\":\"uc-VLUd605CHPd+\",\"name\":\"Itch\",\"added\":[312200],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "771" + } + ], + [ + "user-collections.uc-DU8F01868yMa", + { + "key": "user-collections.uc-DU8F01868yMa", + "timestamp": 1647763461, + "value": "{\"id\":\"uc-DU8F01868yMa\",\"name\":\"TestTag\",\"added\":[],\"removed\":[]}", + "conflictResolutionMethod": "custom", + "strMethodId": "union-collections", + "version": "772" + } + ] +] \ No newline at end of file diff --git a/src/testdata/vdf/user-collections.json b/src/testdata/vdf/user-collections.json new file mode 100644 index 0000000..24c0460 --- /dev/null +++ b/src/testdata/vdf/user-collections.json @@ -0,0 +1,32 @@ +{ + "from-tag-Installed": { + "id": "from-tag-Installed", + "added": [ + 3601140154, + 2719403116 + ], + "removed": [] + }, + "from-tag-Itch": { + "id": "from-tag-Itch", + "added": [ + 3601140154 + ], + "removed": [] + }, + "from-tag-Ready TO Play": { + "id": "from-tag-Ready TO Play", + "added": [ + 3601140154, + 2719403116 + ], + "removed": [] + }, + "from-tag-Heroic": { + "id": "from-tag-Heroic", + "added": [ + 2719403116 + ], + "removed": [] + } +} \ No newline at end of file diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 6cf6427..3f112c3 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -61,6 +61,7 @@ fn empty_or_whitespace(input: String) -> Option { fn update_settings_with_ui_values(settings: &mut Settings, ui: &UserInterface) { // Steam location settings.steam.location = empty_or_whitespace(ui.steam_location_input.value()); + settings.steam.create_collections = ui.steam_create_collections.value(); // Steamgrid db settings.steamgrid_db.enabled = ui.enable_steamgrid_db_checkbox.value(); @@ -116,6 +117,9 @@ fn update_ui_with_settings(ui: &mut UserInterface, settings: &Settings) { .unwrap_or(steam::get_default_location().unwrap_or(String::from(""))), ); + ui.steam_create_collections + .set_value(settings.steam.create_collections); + ui.enable_steamgrid_db_checkbox .set_value(settings.steamgrid_db.enabled); ui.steamgrid_db_auth_key_input.set_value(