diff --git a/src/steam/instaled_games.rs b/src/steam/instaled_games.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/steam/instaled_games.rs @@ -0,0 +1 @@ + diff --git a/src/steam/installed_games.rs b/src/steam/installed_games.rs new file mode 100644 index 0000000..eefa364 --- /dev/null +++ b/src/steam/installed_games.rs @@ -0,0 +1,78 @@ +use std::{ffi::OsStr, path::Path}; + +use super::{get_steam_path, SteamSettings}; + +#[derive(Debug, Clone)] +pub struct SteamGameInfo { + pub appid: u32, + pub name: String, +} + +pub fn get_installed_games(settings: &SteamSettings) -> Vec { + let path = get_steam_path(settings); + if let Ok(path) = path { + let apps_path = Path::new(&path).join("steamapps"); + if let Ok(files) = std::fs::read_dir(apps_path) { + let mut games = vec![]; + for file in files { + if let Ok(file) = file { + if let Some(game_info) = parse_manifest_file(&file.path()) { + games.push(game_info); + } + } + } + return games; + } + } + vec![] +} + +fn parse_manifest_file(path: &Path) -> Option { + let extension = path.extension().and_then(OsStr::to_str); + if let Some("acf") = extension { + let file_content = std::fs::read_to_string(path); + if let Ok(file_content) = file_content { + return parse_manifest_string(file_content); + } + } + None +} + +fn parse_manifest_string>(string: S) -> Option { + let mut lines = string.as_ref().lines(); + let app_id_line = lines.find(|l| l.contains("\"appid\"")); + let name_line = lines.find(|l| l.contains("\"name\"")); + match (app_id_line, name_line) { + (Some(app_id_line), Some(name_line)) => Some(SteamGameInfo { + name: name_line[10..name_line.len() - 1].to_string(), + appid: app_id_line[11..app_id_line.len() - 1] + .to_string() + .parse() + .unwrap(), + }), + _ => None, + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn parse_steam_game() { + let string = include_str!("../testdata/acf/appmanifest_763890.acf"); + let game_info = parse_manifest_string(string); + assert!(game_info.is_some()); + let game_info = game_info.unwrap(); + assert_eq!("Wildermyth", game_info.name); + assert_eq!(763890, game_info.appid); + } + + // #[test] + // fn installed_files() { + // let settings = SteamSettings::default(); + // let installed_games = get_installed_games(&settings); + // assert_eq!(installed_games.len(), 7); + // } +} diff --git a/src/steam/mod.rs b/src/steam/mod.rs index 4623fb2..58730e4 100644 --- a/src/steam/mod.rs +++ b/src/steam/mod.rs @@ -1,10 +1,12 @@ mod collections; +mod installed_games; mod proton_vdf_util; mod restarter; mod settings; mod utils; pub use collections::*; +pub use installed_games::*; pub use proton_vdf_util::*; pub use restarter::*; pub use settings::SteamSettings; diff --git a/src/steam/settings.rs b/src/steam/settings.rs index 0bc53c8..79cc9b3 100644 --- a/src/steam/settings.rs +++ b/src/steam/settings.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct SteamSettings { pub location: Option, pub create_collections: bool, diff --git a/src/testdata/acf/appmanifest_763890.acf b/src/testdata/acf/appmanifest_763890.acf new file mode 100755 index 0000000..ee055ad --- /dev/null +++ b/src/testdata/acf/appmanifest_763890.acf @@ -0,0 +1,32 @@ +"AppState" +{ + "appid" "763890" + "universe" "1" + "name" "Wildermyth" + "StateFlags" "4" + "installdir" "Wildermyth" + "LastUpdated" "1651783287" + "UpdateResult" "0" + "SizeOnDisk" "2035740746" + "buildid" "8621142" + "LastOwner" "76561197970608363" + "BytesToDownload" "1706137168" + "BytesDownloaded" "1706137168" + "BytesToStage" "2035740746" + "BytesStaged" "2035740746" + "AutoUpdateBehavior" "0" + "AllowOtherDownloadsWhileRunning" "0" + "ScheduledAutoUpdate" "0" + "InstalledDepots" + { + "763893" + { + "manifest" "8422302896250419544" + "size" "2035740746" + } + } + "UserConfig" + { + "language" "english" + } +} diff --git a/src/ui/ui_image_download.rs b/src/ui/ui_image_download.rs index 8a44a57..8c770d1 100644 --- a/src/ui/ui_image_download.rs +++ b/src/ui/ui_image_download.rs @@ -7,6 +7,7 @@ use crate::{ config::get_thumbnails_folder, steam::{get_shortcuts_paths, SteamUsersInfo}, steamgriddb::{get_image_extension, get_query_type, CachedSearch, ImageType, ToDownload}, + steam::{get_installed_games, SteamGameInfo}, }; use dashmap::DashMap; use egui::{ImageButton, ScrollArea}; @@ -18,16 +19,16 @@ use tokio::sync::watch::{self, Receiver}; use super::{ui_images::load_image_from_path, FetcStatus, MyEguiApp}; pub struct ImageSelectState { - pub selected_shortcut: Option, + pub selected_shortcut: Option, pub grid_id: Option, pub steam_user: Option, pub steam_users: Option>, pub user_shortcuts: Option>, - + pub game_mode: GameMode, pub image_type_selected: Option, pub image_options: Receiver>>, - + pub steam_games: Option>, pub image_handles: std::sync::Arc>, pub possible_names: Option>, @@ -40,6 +41,21 @@ pub enum TextureState { Loaded(egui::TextureHandle), } +#[derive(Debug)] +pub enum GameMode { + Shortcuts, + SteamGames, +} + +impl GameMode { + pub fn is_shortcuts(&self) -> bool { + match self { + GameMode::Shortcuts => true, + GameMode::SteamGames => false, + } + } +} + impl ImageSelectState { pub fn has_multiple_users(&self) -> bool { match &self.steam_users { @@ -66,10 +82,34 @@ impl Default for ImageSelectState { steam_user: Default::default(), steam_users: Default::default(), user_shortcuts: Default::default(), + game_mode: GameMode::Shortcuts, image_type_selected: Default::default(), possible_names: None, image_options: watch::channel(FetcStatus::NeedsFetched).1, image_handles: Arc::new(DashMap::new()), + steam_games: None, + } + } +} + +#[derive(Debug, Clone)] +pub enum GameType { + Shortcut(ShortcutOwned), + SteamGame(SteamGameInfo), +} + +impl GameType { + pub fn app_id(&self) -> u32 { + match self { + GameType::Shortcut(shortcut) => shortcut.app_id, + GameType::SteamGame(game) => game.appid, + } + } + + pub fn name(&self) -> &str { + match self { + GameType::Shortcut(s) => s.app_name.as_ref(), + GameType::SteamGame(g) => g.name.as_ref(), } } } @@ -78,11 +118,12 @@ impl Default for ImageSelectState { enum UserAction { CorrectGridId, UserSelected(SteamUsersInfo), - ShortcutSelected(ShortcutOwned), + ShortcutSelected(GameType), ImageTypeSelected(ImageType), ImageTypeCleared(ImageType, bool), ImageSelected(PossibleImage), GridIdChanged(usize), + SetGamesMode(GameMode), BackButton, NoAction, } @@ -101,7 +142,7 @@ impl MyEguiApp { return render_user_select(state, ui); } if let Some(shortcut) = state.selected_shortcut.as_ref() { - ui.heading(&shortcut.app_name); + ui.heading(shortcut.name()); if let Some(possible_names) = state.possible_names.as_ref() { if let Some(value) = render_possible_names(possible_names, ui) { @@ -112,12 +153,36 @@ impl MyEguiApp { if let Some(action) = self.render_possible_images(ui, image_type, state) { return action; } - } else if let Some(action) = render_shortcut_images(ui, state) { + } else { + if let Some(action) = render_shortcut_images(ui, state) { + return action; + } + } + } + } else { + let is_shortcut = state.game_mode.is_shortcuts(); + if ui + .selectable_label(is_shortcut, "Images for shortcuts") + .clicked() + { + return UserAction::SetGamesMode(GameMode::Shortcuts); + } + + if ui + .selectable_label(!is_shortcut, "Images for steam games") + .clicked() + { + return UserAction::SetGamesMode(GameMode::SteamGames); + } + if is_shortcut { + if let Some(action) = self.render_shortcut_select(ui) { + return action; + } + } else { + if let Some(action) = render_steam_game_select(ui, state) { return action; } } - } else if let Some(action) = self.render_shortcut_select(ui) { - return action; } UserAction::NoAction } @@ -150,7 +215,9 @@ impl MyEguiApp { let button = ui.button(&shortcut.app_name); clicked = clicked || button.clicked(); if clicked { - return Some(UserAction::ShortcutSelected(shortcut.clone())); + return Some(UserAction::ShortcutSelected(GameType::Shortcut( + shortcut.clone(), + ))); } } } @@ -296,7 +363,9 @@ impl MyEguiApp { UserAction::GridIdChanged(grid_id) => { self.handle_grid_change(grid_id); } - + UserAction::SetGamesMode(game_mode) => { + self.handle_set_game_mode(game_mode); + } UserAction::NoAction => {} UserAction::CorrectGridId => { self.handle_correct_grid_request(); @@ -307,7 +376,7 @@ impl MyEguiApp { .selected_shortcut .as_ref() .unwrap() - .app_id; + .app_id(); self.settings .steamgrid_db .set_image_banned(&image_type, app_id, should_ban); @@ -329,7 +398,7 @@ impl MyEguiApp { .selected_shortcut .as_ref() .unwrap() - .app_id, + .app_id(), ext, ); let path = Path::new(data_folder) @@ -350,7 +419,7 @@ impl MyEguiApp { .image_selected_state .selected_shortcut .as_ref() - .map(|s| s.app_name.clone()) + .map(|s| s.name().clone()) .unwrap_or_default(); let auth_key = self .settings @@ -363,6 +432,10 @@ impl MyEguiApp { self.image_selected_state.possible_names = search_results.ok(); } + fn handle_set_game_mode(&mut self, game_mode: GameMode) { + self.image_selected_state.game_mode = game_mode; + self.image_selected_state.steam_games = Some(get_installed_games(&self.settings.steam)); + } fn handle_grid_change(&mut self, grid_id: usize) { self.image_selected_state.grid_id = Some(grid_id); self.image_selected_state.possible_names = None; @@ -370,7 +443,7 @@ impl MyEguiApp { let client = steamgriddb_api::Client::new(auth_key); let mut cache = CachedSearch::new(&client); if let Some(shortcut) = &self.image_selected_state.selected_shortcut { - cache.set_cache(shortcut.app_id, shortcut.app_name.clone(), grid_id); + cache.set_cache(shortcut.app_id(), shortcut.name().clone(), grid_id); cache.save(); } } @@ -456,7 +529,7 @@ impl MyEguiApp { let to = Path::new(&user.steam_user_data_folder) .join("config") .join("grid") - .join(selected_image_type.file_name(selected_image.app_id, ext)); + .join(selected_image_type.file_name(selected_image.app_id(), ext)); if to.exists() { let old_key = to.to_string_lossy().to_string(); @@ -470,11 +543,11 @@ impl MyEguiApp { } } - let app_name = selected_image.app_name.clone(); + let app_name = selected_image.name(); let to_download = ToDownload { path: to, url: image.full_url.clone(), - app_name, + app_name: app_name.to_string(), image_type: *selected_image_type, }; self.rt.spawn_blocking(move || { @@ -500,7 +573,7 @@ impl MyEguiApp { } } - fn handle_shortcut_selected(&mut self, shortcut: ShortcutOwned, ui: &mut egui::Ui) { + fn handle_shortcut_selected(&mut self, shortcut: GameType, ui: &mut egui::Ui) { let state = &mut self.image_selected_state; //We must have a user to make see this action; let user = state.steam_user.as_ref().unwrap(); @@ -509,7 +582,7 @@ impl MyEguiApp { let search = CachedSearch::new(&client); state.grid_id = self .rt - .block_on(search.search(shortcut.app_id, &shortcut.app_name)) + .block_on(search.search(shortcut.app_id(), shortcut.name())) .ok() .flatten(); } @@ -556,6 +629,19 @@ fn render_possible_names( None } +fn render_steam_game_select(ui: &mut egui::Ui, state: &ImageSelectState) -> Option { + if let Some(steam_games) = state.steam_games.as_ref() { + for game in steam_games { + if ui.button(&game.name).clicked() { + return Some(UserAction::ShortcutSelected(GameType::SteamGame( + game.clone(), + ))); + } + } + } + None +} + fn render_shortcut_images(ui: &mut egui::Ui, state: &ImageSelectState) -> Option { let mut grid_id_text = state.grid_id.map(|id| id.to_string()).unwrap_or_default(); if ui.text_edit_singleline(&mut grid_id_text).changed() { @@ -635,11 +721,31 @@ trait HasImageKey { } const POSSIBLE_EXTENSIONS: [&'static str; 4] = ["png", "jpg", "ico", "webp"]; +impl HasImageKey for GameType { + fn key(&self, image_type: &ImageType, user_path: &Path) -> (PathBuf, String) { + match self { + GameType::Shortcut(s) => s.key(image_type, user_path), + GameType::SteamGame(g) => g.key(image_type, user_path), + } + } +} +impl HasImageKey for SteamGameInfo { + fn key(&self, image_type: &ImageType, user_path: &Path) -> (PathBuf, String) { + let mut keys = POSSIBLE_EXTENSIONS + .iter() + .map(|ext| key_from_extension(self.appid, image_type, user_path, ext)); + let first = keys.next().unwrap(); + let other = keys.filter(|(exsists, _, _)| *exsists).next(); + let (_, path, key) = other.unwrap_or(first); + (path, key) + } +} + impl HasImageKey for ShortcutOwned { fn key(&self, image_type: &ImageType, user_path: &Path) -> (PathBuf, String) { let mut keys = POSSIBLE_EXTENSIONS .iter() - .map(|ext| key_from_extension(self, image_type, user_path, ext)); + .map(|ext| key_from_extension(self.app_id, image_type, user_path, ext)); let first = keys.next().unwrap(); let other = keys.filter(|(exsists, _, _)| *exsists).next(); let (_, path, key) = other.unwrap_or(first); @@ -648,12 +754,12 @@ impl HasImageKey for ShortcutOwned { } fn key_from_extension( - shortcut: &ShortcutOwned, + app_id: u32, image_type: &ImageType, user_path: &Path, ext: &str, ) -> (bool, PathBuf, String) { - let file_name = image_type.file_name(shortcut.app_id, ext); + let file_name = image_type.file_name(app_id, ext); let path = user_path.join("config").join("grid").join(&file_name); let key = path.to_string_lossy().to_string(); (path.exists(), path, key)