From 7a5c88c61ee14fffbc8cf4ca692692d9f64ac845 Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Thu, 29 Dec 2022 09:36:36 +0100 Subject: [PATCH] Move image render functions into pages (#291) Move image page renderes into seperate files --- src/ui/images/constants.rs | 3 + src/ui/images/gamemode.rs | 22 + src/ui/images/gametype.rs | 27 + src/ui/images/hasimagekey.rs | 62 + src/ui/images/image_resize.rs | 11 + src/ui/images/image_select_state.rs | 48 + src/ui/images/mod.rs | 14 + src/ui/images/pages/change_grid_db_id.rs | 33 + src/ui/images/pages/mod.rs | 12 + src/ui/images/pages/pick_new_image.rs | 165 +++ src/ui/images/pages/select_image_type.rs | 131 ++ .../images/pages/shortcut_images_overview.rs | 95 ++ src/ui/images/pages/steam_images_overview.rs | 14 + src/ui/images/possible_image.rs | 12 + src/ui/images/texturestate.rs | 7 + src/ui/images/ui_image_download.rs | 528 ++++++++ src/ui/images/useraction.rs | 21 + src/ui/mod.rs | 3 +- src/ui/ui_image_download.rs | 1086 ----------------- src/ui/uiapp.rs | 2 +- 20 files changed, 1207 insertions(+), 1089 deletions(-) create mode 100644 src/ui/images/constants.rs create mode 100644 src/ui/images/gamemode.rs create mode 100644 src/ui/images/gametype.rs create mode 100644 src/ui/images/hasimagekey.rs create mode 100644 src/ui/images/image_resize.rs create mode 100644 src/ui/images/image_select_state.rs create mode 100644 src/ui/images/mod.rs create mode 100644 src/ui/images/pages/change_grid_db_id.rs create mode 100644 src/ui/images/pages/mod.rs create mode 100644 src/ui/images/pages/pick_new_image.rs create mode 100644 src/ui/images/pages/select_image_type.rs create mode 100644 src/ui/images/pages/shortcut_images_overview.rs create mode 100644 src/ui/images/pages/steam_images_overview.rs create mode 100644 src/ui/images/possible_image.rs create mode 100644 src/ui/images/texturestate.rs create mode 100644 src/ui/images/ui_image_download.rs create mode 100644 src/ui/images/useraction.rs delete mode 100644 src/ui/ui_image_download.rs diff --git a/src/ui/images/constants.rs b/src/ui/images/constants.rs new file mode 100644 index 0000000..33b2379 --- /dev/null +++ b/src/ui/images/constants.rs @@ -0,0 +1,3 @@ + +pub const POSSIBLE_EXTENSIONS: [&str; 4] = ["png", "jpg", "ico", "webp"]; +pub const MAX_WIDTH: f32 = 300.; \ No newline at end of file diff --git a/src/ui/images/gamemode.rs b/src/ui/images/gamemode.rs new file mode 100644 index 0000000..c2adbe0 --- /dev/null +++ b/src/ui/images/gamemode.rs @@ -0,0 +1,22 @@ + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GameMode { + Shortcuts, + SteamGames, +} + +impl GameMode { + pub fn is_shortcuts(&self) -> bool { + match self { + GameMode::Shortcuts => true, + GameMode::SteamGames => false, + } + } + + pub fn label(&self) -> &str { + match self { + GameMode::Shortcuts => "Images for shortcuts", + GameMode::SteamGames => "Images for steam games", + } + } +} \ No newline at end of file diff --git a/src/ui/images/gametype.rs b/src/ui/images/gametype.rs new file mode 100644 index 0000000..482ee2b --- /dev/null +++ b/src/ui/images/gametype.rs @@ -0,0 +1,27 @@ + +use steam_shortcuts_util::shortcut::ShortcutOwned; + +use crate::{steam::SteamGameInfo}; + + +#[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(), + } + } +} diff --git a/src/ui/images/hasimagekey.rs b/src/ui/images/hasimagekey.rs new file mode 100644 index 0000000..d3f607c --- /dev/null +++ b/src/ui/images/hasimagekey.rs @@ -0,0 +1,62 @@ +use std::path::{Path, PathBuf}; + +use steam_shortcuts_util::shortcut::ShortcutOwned; + +use crate::{steamgriddb::ImageType, steam::SteamGameInfo}; + +use super::{gametype::GameType, constants::POSSIBLE_EXTENSIONS}; + + +pub trait HasImageKey { + + ///Gives a unique key to an image given its type and user path + fn key(&self, image_type: &ImageType, user_path: &Path) -> (PathBuf, String); + +} + + +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.find(|(exsists, _, _)| *exsists); + 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.app_id, image_type, user_path, ext)); + let first = keys.next().unwrap(); + let other = keys.find(|(exsists, _, _)| *exsists); + let (_, path, key) = other.unwrap_or(first); + (path, key) + } +} + +fn key_from_extension( + app_id: u32, + image_type: &ImageType, + user_path: &Path, + ext: &str, +) -> (bool, PathBuf, String) { + 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) +} diff --git a/src/ui/images/image_resize.rs b/src/ui/images/image_resize.rs new file mode 100644 index 0000000..b3cd86b --- /dev/null +++ b/src/ui/images/image_resize.rs @@ -0,0 +1,11 @@ +pub fn clamp_to_width(size: &mut egui::Vec2, max_width: f32) { + let mut x = size.x; + let mut y = size.y; + if size.x > max_width { + let ratio = size.y / size.x; + x = max_width; + y = x * ratio; + } + size.x = x; + size.y = y; +} \ No newline at end of file diff --git a/src/ui/images/image_select_state.rs b/src/ui/images/image_select_state.rs new file mode 100644 index 0000000..ef85be8 --- /dev/null +++ b/src/ui/images/image_select_state.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use steam_shortcuts_util::shortcut::ShortcutOwned; + +use dashmap::DashMap; +use crate::{steam::SteamUsersInfo, steamgriddb::ImageType, ui::FetcStatus}; + +use super::{ gamemode::GameMode, possible_image::PossibleImage, texturestate::TextureDownloadState, gametype::GameType}; + +use tokio::sync::watch::{self, Receiver}; +pub struct ImageSelectState { + pub selected_shortcut: Option, + pub grid_id: Option, + + pub steam_user: Option, + pub settings_error: 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>, +} + + + +impl Default for ImageSelectState { + fn default() -> Self { + Self { + selected_shortcut: Default::default(), + grid_id: Default::default(), + steam_user: Default::default(), + steam_users: Default::default(), + settings_error: 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, + } + } +} + diff --git a/src/ui/images/mod.rs b/src/ui/images/mod.rs new file mode 100644 index 0000000..752d3f9 --- /dev/null +++ b/src/ui/images/mod.rs @@ -0,0 +1,14 @@ +mod texturestate; +mod ui_image_download; +mod gamemode; +mod possible_image; +mod image_select_state; +mod gametype; +mod useraction; +mod hasimagekey; +mod image_resize; +mod constants; + +mod pages; + +pub use image_select_state::ImageSelectState; \ No newline at end of file diff --git a/src/ui/images/pages/change_grid_db_id.rs b/src/ui/images/pages/change_grid_db_id.rs new file mode 100644 index 0000000..1bea0c8 --- /dev/null +++ b/src/ui/images/pages/change_grid_db_id.rs @@ -0,0 +1,33 @@ +use crate::ui::images::{image_select_state::ImageSelectState, useraction::UserAction}; + + +pub fn render_page_change_grid_db_id( + possible_names: &Vec, + ui: &mut egui::Ui, + state: &ImageSelectState, +) -> Option { + let mut grid_id_text = state.grid_id.map(|id| id.to_string()).unwrap_or_default(); + ui.label("SteamGridDB ID") + .on_hover_text("You can change this id to one you have found at the steamgriddb webpage"); + if ui.text_edit_singleline(&mut grid_id_text).changed() { + if let Ok(grid_id) = grid_id_text.parse::() { + return Some(UserAction::GridIdChanged(grid_id)); + } + }; + + for possible in possible_names { + if ui.button(&possible.name).clicked() { + return Some(UserAction::GridIdChanged(possible.id)); + } + } + + ui.separator(); + if ui + .button("Clear all images") + .on_hover_text("Clicking this deletes all images for this shortcut") + .clicked() + { + return Some(UserAction::ClearImages); + } + None +} \ No newline at end of file diff --git a/src/ui/images/pages/mod.rs b/src/ui/images/pages/mod.rs new file mode 100644 index 0000000..97e2019 --- /dev/null +++ b/src/ui/images/pages/mod.rs @@ -0,0 +1,12 @@ +mod change_grid_db_id; +mod shortcut_images_overview; +mod steam_images_overview; +mod select_image_type; +mod pick_new_image; + + +pub use change_grid_db_id::render_page_change_grid_db_id; +pub use shortcut_images_overview::render_page_shortcut_images_overview; +pub use steam_images_overview::render_page_steam_images_overview; +pub use select_image_type::render_page_shortcut_select_image_type; +pub use pick_new_image::render_page_pick_image; \ No newline at end of file diff --git a/src/ui/images/pages/pick_new_image.rs b/src/ui/images/pages/pick_new_image.rs new file mode 100644 index 0000000..47ba207 --- /dev/null +++ b/src/ui/images/pages/pick_new_image.rs @@ -0,0 +1,165 @@ +use egui::{Grid, ImageButton}; +use futures::executor::block_on; + +use crate::{ + steamgriddb::{ImageType, ToDownload}, + ui::{ + images::{ + constants::MAX_WIDTH, image_resize::clamp_to_width, + image_select_state::ImageSelectState, texturestate::TextureDownloadState, + useraction::UserAction, + }, + ui_images::load_image_from_path, + FetcStatus, MyEguiApp, + }, +}; + +pub fn render_page_pick_image( + app: &MyEguiApp, + ui: &mut egui::Ui, + image_type: &ImageType, + state: &ImageSelectState, +) -> Option { + ui.label(image_type.name()); + + if let Some(action) = ui + .horizontal(|ui| { + if ui + .small_button("Clear image?") + .on_hover_text("Click here to clear the image") + .clicked() + { + return Some(UserAction::ImageTypeCleared(*image_type, false)); + } + + if ui + .small_button("Stop downloading this image?") + .on_hover_text("Stop downloading this type of image for this shortcut at all") + .clicked() + { + return Some(UserAction::ImageTypeCleared(*image_type, true)); + } + None + }) + .inner + { + return Some(action); + } + let column_padding = 10.; + let column_width = MAX_WIDTH * 0.75; + let width = ui.available_width(); + let columns = (width / (column_width + column_padding)).floor() as u32; + let mut column = 0; + match &*state.image_options.borrow() { + FetcStatus::Fetched(images) => { + let x = Grid::new("ImageThumbnailSelectGrid") + .spacing([column_padding, column_padding]) + .show(ui, |ui| { + for image in images { + let image_key = + image.thumbnail_path.as_path().to_string_lossy().to_string(); + + match state.image_handles.get_mut(&image_key) { + Some(mut state) => { + match state.value() { + TextureDownloadState::Downloading => { + ui.ctx().request_repaint(); + //nothing to do,just wait + ui.spinner(); + } + TextureDownloadState::Downloaded => { + //Need to load + let image_data = + load_image_from_path(&image.thumbnail_path); + match image_data { + Ok(image_data) => { + let handle = ui.ctx().load_texture( + &image_key, + image_data, + egui::TextureOptions::LINEAR, + ); + *state.value_mut() = + TextureDownloadState::Loaded(handle); + ui.spinner(); + } + Err(_) => { + *state.value_mut() = TextureDownloadState::Failed + } + } + ui.ctx().request_repaint(); + } + TextureDownloadState::Loaded(texture_handle) => { + //need to show + let mut size = texture_handle.size_vec2(); + clamp_to_width(&mut size, column_width); + let image_button = ImageButton::new(texture_handle, size); + if ui.add_sized(size, image_button).clicked() { + return Some(UserAction::ImageSelected(image.clone())); + } + } + TextureDownloadState::Failed => { + ui.label("Failed to load image"); + } + } + } + None => { + //We need to start a download + let image_handles = &app.image_selected_state.image_handles; + let path = &image.thumbnail_path; + //Redownload if file is too small + if !path.exists() + || std::fs::metadata(path).map(|m| m.len()).unwrap_or_default() + < 2 + { + image_handles.insert( + image_key.clone(), + TextureDownloadState::Downloading, + ); + let to_download = ToDownload { + path: path.clone(), + url: image.thumbnail_url.clone(), + app_name: "Thumbnail".to_string(), + image_type: *image_type, + }; + let image_handles = image_handles.clone(); + let image_key = image_key.clone(); + app.rt.spawn_blocking(move || { + block_on(crate::steamgriddb::download_to_download( + &to_download, + )) + .unwrap(); + image_handles + .insert(image_key, TextureDownloadState::Downloaded); + }); + } else { + image_handles.insert( + image_key.clone(), + TextureDownloadState::Downloaded, + ); + } + } + } + column += 1; + if column >= columns { + column = 0; + ui.end_row(); + } + } + + None + }) + .inner; + if x.is_some() { + return x; + } + } + _ => { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Finding possible images"); + }); + ui.ctx().request_repaint(); + } + } + None +} diff --git a/src/ui/images/pages/select_image_type.rs b/src/ui/images/pages/select_image_type.rs new file mode 100644 index 0000000..03cb8b1 --- /dev/null +++ b/src/ui/images/pages/select_image_type.rs @@ -0,0 +1,131 @@ +use std::path::Path; + +use egui::ImageButton; + +use crate::{ui::images::{image_select_state::ImageSelectState, useraction::UserAction, gametype::GameType, texturestate::TextureDownloadState, hasimagekey::HasImageKey, image_resize::clamp_to_width}, steamgriddb::ImageType}; + +const MAX_WIDTH: f32 = 300.; + + +pub fn render_page_shortcut_select_image_type(ui: &mut egui::Ui, state: &ImageSelectState) -> Option { + let shortcut = state.selected_shortcut.as_ref().unwrap(); + let user_path = &state.steam_user.as_ref().unwrap().steam_user_data_folder; + let x = if ui.available_width() > MAX_WIDTH * 3. { + ui.horizontal(|ui| { + let x = ui + .vertical(|ui| { + let texture = + texture_from_iamge_type(shortcut, &ImageType::Grid, user_path, state); + ui.label(ImageType::Grid.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::Grid)); + } + None + }) + .inner; + if x.is_some() { + return x; + } + let x = ui + .vertical(|ui| { + let texture = + texture_from_iamge_type(shortcut, &ImageType::Hero, user_path, state); + ui.label(ImageType::Hero.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::Hero)); + } + let texture = + texture_from_iamge_type(shortcut, &ImageType::WideGrid, user_path, state); + ui.label(ImageType::WideGrid.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::WideGrid)); + } + + let texture = + texture_from_iamge_type(shortcut, &ImageType::Logo, user_path, state); + ui.label(ImageType::Logo.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::Logo)); + } + None + }) + .inner; + if x.is_some() { + return x; + } + ui.vertical(|ui| { + let texture = texture_from_iamge_type(shortcut, &ImageType::Icon, user_path, state); + ui.label(ImageType::Icon.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::Icon)); + } + + let texture = + texture_from_iamge_type(shortcut, &ImageType::BigPicture, user_path, state); + ui.label(ImageType::BigPicture.name()); + if render_thumbnail(ui, texture).clicked() { + return Some(UserAction::ImageTypeSelected(ImageType::BigPicture)); + } + None + }) + .inner + }) + .inner + } else { + render_image_types_as_list(shortcut, user_path, state, ui) + }; + + if ui + .button("Click here if the images are for a wrong game") + .clicked() + { + return Some(UserAction::CorrectGridId); + } + x +} + +fn render_image_types_as_list( + shortcut: &GameType, + user_path: &String, + state: &ImageSelectState, + ui: &mut egui::Ui, +) -> Option { + let types = ImageType::all(); + for image_type in types { + let texture = texture_from_iamge_type(shortcut, image_type, user_path, state); + let response = ui + .vertical(|ui| { + ui.label(image_type.name()); + render_thumbnail(ui, texture) + }) + .inner; + if response.clicked() { + return Some(UserAction::ImageTypeSelected(*image_type)); + } + } + None +} + +fn texture_from_iamge_type( + shortcut: &GameType, + image_type: &ImageType, + user_path: &String, + state: &ImageSelectState, +) -> Option { + let (_path, key) = shortcut.key(image_type, Path::new(&user_path)); + state.image_handles.get(&key).and_then(|k| match k.value() { + TextureDownloadState::Loaded(texture) => Some(texture.clone()), + _ => None, + }) +} + +fn render_thumbnail(ui: &mut egui::Ui, image: Option) -> egui::Response { + if let Some(texture) = image { + let mut size = texture.size_vec2(); + clamp_to_width(&mut size, MAX_WIDTH); + let image_button = ImageButton::new(&texture, size); + ui.add(image_button) + } else { + ui.button("Pick an image") + } +} diff --git a/src/ui/images/pages/shortcut_images_overview.rs b/src/ui/images/pages/shortcut_images_overview.rs new file mode 100644 index 0000000..c8c7736 --- /dev/null +++ b/src/ui/images/pages/shortcut_images_overview.rs @@ -0,0 +1,95 @@ +use std::path::Path; + +use egui::{Button, ImageButton}; +use steam_shortcuts_util::shortcut::ShortcutOwned; + +use crate::{ + steam::SteamUsersInfo, + steamgriddb::ImageType, + ui::{ + images::{ + gametype::GameType, hasimagekey::HasImageKey, image_resize::clamp_to_width, + texturestate::TextureDownloadState, useraction::UserAction, + }, + MyEguiApp, + }, +}; + +pub fn render_page_shortcut_images_overview(app: &MyEguiApp, ui: &mut egui::Ui) -> Option { + let shortcuts = &app.image_selected_state.user_shortcuts; + + let width = ui.available_size().x; + let column_width = 100.; + let column_padding = 23.; + let columns = (width / (column_width + column_padding)).floor() as u32; + let mut cur_column = 0; + match shortcuts { + Some(shortcuts) => { + let user_info = &app.image_selected_state.steam_user.as_ref().unwrap(); + if let Some(action) = egui::Grid::new("ui_images") + .show(ui, |ui| { + for shortcut in shortcuts { + let action = render_image(app, shortcut, user_info, column_width, ui); + if action.is_some() { + return action; + } + cur_column += 1; + if cur_column >= columns { + cur_column = 0; + ui.end_row(); + } + } + ui.end_row(); + None + }) + .inner + { + return action; + } + } + None => { + ui.label("Could not find any shortcuts"); + } + } + None +} + +fn render_image( + app: &MyEguiApp, + shortcut: &ShortcutOwned, + user_info: &&SteamUsersInfo, + column_width: f32, + ui: &mut egui::Ui, +) -> Option> { + let (_, key) = shortcut.key( + &ImageType::Grid, + Path::new(&user_info.steam_user_data_folder), + ); + let mut clicked = false; + + let texture = app.image_selected_state.image_handles.get(&key); + if let Some(texture) = texture { + if let TextureDownloadState::Loaded(texture) = &texture.value() { + let mut size = texture.size_vec2(); + clamp_to_width(&mut size, column_width); + let image_button = ImageButton::new(texture, size); + clicked = ui + .add(image_button) + .on_hover_text(&shortcut.app_name) + .clicked(); + } + } else { + let button = ui.add_sized( + [column_width, column_width * 1.6], + Button::new(&shortcut.app_name).wrap(true), + ); + clicked = clicked || button.clicked(); + } + + if clicked { + return Some(Some(UserAction::ShortcutSelected(GameType::Shortcut( + shortcut.clone(), + )))); + } + None +} diff --git a/src/ui/images/pages/steam_images_overview.rs b/src/ui/images/pages/steam_images_overview.rs new file mode 100644 index 0000000..dc27bdd --- /dev/null +++ b/src/ui/images/pages/steam_images_overview.rs @@ -0,0 +1,14 @@ +use crate::ui::images::{useraction::UserAction, gametype::GameType, image_select_state::ImageSelectState}; + +pub fn render_page_steam_images_overview(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 +} \ No newline at end of file diff --git a/src/ui/images/possible_image.rs b/src/ui/images/possible_image.rs new file mode 100644 index 0000000..5db62d9 --- /dev/null +++ b/src/ui/images/possible_image.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +use steamgriddb_api::images::MimeTypes; + + +#[derive(Clone, Debug)] +pub struct PossibleImage { + pub thumbnail_path: PathBuf, + pub thumbnail_url: String, + pub mime: MimeTypes, + pub full_url: String, +} \ No newline at end of file diff --git a/src/ui/images/texturestate.rs b/src/ui/images/texturestate.rs new file mode 100644 index 0000000..be513fa --- /dev/null +++ b/src/ui/images/texturestate.rs @@ -0,0 +1,7 @@ +#[derive(Clone)] +pub enum TextureDownloadState { + Downloading, + Downloaded, + Loaded(egui::TextureHandle), + Failed, +} diff --git a/src/ui/images/ui_image_download.rs b/src/ui/images/ui_image_download.rs new file mode 100644 index 0000000..ec0aac3 --- /dev/null +++ b/src/ui/images/ui_image_download.rs @@ -0,0 +1,528 @@ +use super::{texturestate::TextureDownloadState, gamemode::GameMode, possible_image::PossibleImage, gametype::GameType, pages::{render_page_shortcut_images_overview, render_page_steam_images_overview, render_page_pick_image, render_page_shortcut_select_image_type}, useraction::UserAction, constants::POSSIBLE_EXTENSIONS, hasimagekey::HasImageKey, image_select_state::ImageSelectState}; + +use std::{ + path::{Path}, +}; + +use crate::{ + config::get_thumbnails_folder, + steam::{get_installed_games, SteamUsersInfo,}, + steam::{get_shortcuts_paths }, + steamgriddb::{get_image_extension, get_query_type, CachedSearch, ImageType, ToDownload}, + sync::{download_images, SyncProgress}, ui::{MyEguiApp, FetcStatus, ui_images::load_image_from_path}, +}; +use egui::{ScrollArea}; +use futures::executor::block_on; +use steam_shortcuts_util::shortcut::ShortcutOwned; +use tokio::sync::watch; + + + + +impl MyEguiApp { + fn render_ui_image_action(&self, ui: &mut egui::Ui) -> UserAction { + let state = &self.image_selected_state; + if let Some(action) = ui + .horizontal(|ui| { + let render_back = + state.selected_shortcut.is_some() || state.image_type_selected.is_some(); + if render_back { + if ui.button("Back").clicked() { + return Some(UserAction::BackButton); + } + None + } else { + if let Some(value) = render_user_select(state, ui) { + return Some(value); + } + render_shortcut_mode_select(state, ui) + } + }) + .inner + { + return action; + } + + if let Some(shortcut) = state.selected_shortcut.as_ref() { + ui.heading(shortcut.name()); + + if let Some(possible_names) = state.possible_names.as_ref() { + if let Some(value) = super::pages::render_page_change_grid_db_id(possible_names, ui, state) { + return value; + } + } else if let Some(image_type) = state.image_type_selected.as_ref() { + if let Some(action) = render_page_pick_image(&self,ui, image_type, state) { + return action; + } + } else if let Some(action) = render_page_shortcut_select_image_type(ui, state) { + return action; + } + } else { + let is_shortcut = state.game_mode.is_shortcuts(); + if is_shortcut { + if let Some(action) = render_page_shortcut_images_overview(&self,ui) { + return action; + } + } else if let Some(action) = render_page_steam_images_overview(ui, state) { + return action; + } + + if let Some(value) = self.render_find_all_images(ui) { + return value; + } + } + + UserAction::NoAction + } + + fn render_find_all_images(&self, ui: &mut egui::Ui) -> Option { + match *self.status_reciever.borrow() { + crate::sync::SyncProgress::FindingImages => { + ui.spinner(); + ui.label("Finding images to download"); + ui.ctx().request_repaint(); + } + crate::sync::SyncProgress::DownloadingImages { to_download } => { + ui.spinner(); + ui.label(format!("Downloading {to_download} images")); + ui.ctx().request_repaint(); + } + crate::sync::SyncProgress::Done => { + ui.ctx().request_repaint(); + return Some(UserAction::RefreshImages); + } + _ => { + if ui.button("Download images for all games").clicked() { + return Some(UserAction::DownloadAllImages); + } + } + } + None + } + + + + + fn ensure_steam_users_loaded(&mut self) { + if self.image_selected_state.settings_error.is_none() + && self.image_selected_state.steam_users.is_none() + { + let paths = get_shortcuts_paths(&self.settings.steam); + match paths { + Ok(paths) => self.image_selected_state.steam_users = Some(paths), + Err(err) => { + self.image_selected_state.settings_error = Some(format!("Could not find user steam location, error message: {} , try to clear the steam location field in settings to let BoilR find it itself",err)); + } + } + } + } + + pub fn render_ui_images(&mut self, ui: &mut egui::Ui) { + self.ensure_steam_users_loaded(); + + if let Some(error_message) = &self.image_selected_state.settings_error { + ui.label(error_message); + return; + } + + let mut action = UserAction::NoAction; + ScrollArea::vertical() + .stick_to_right(true) + .auto_shrink([false, true]) + .show(ui, |ui| { + ui.reset_style(); + action = self.render_ui_image_action(ui); + }); + match action { + UserAction::UserSelected(user) => { + self.handle_user_selected(user, ui); + } + UserAction::ShortcutSelected(shortcut) => { + self.handle_shortcut_selected(shortcut, ui); + } + UserAction::ImageTypeSelected(image_type) => { + self.handle_image_type_selected(image_type); + } + UserAction::ImageSelected(image) => { + self.handle_image_selected(image); + } + UserAction::BackButton => { + self.handle_back_button_action(); + } + 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(); + } + UserAction::ImageTypeCleared(image_type, should_ban) => { + let app_id = self + .image_selected_state + .selected_shortcut + .as_ref() + .unwrap() + .app_id(); + self.settings + .steamgrid_db + .set_image_banned(&image_type, app_id, should_ban); + self.handle_image_type_clear(image_type); + } + UserAction::ClearImages => { + for image_type in ImageType::all() { + self.handle_image_type_clear(*image_type); + } + self.handle_back_button_action(); + } + UserAction::DownloadAllImages => { + if let Some(users) = &self.image_selected_state.steam_users { + let (sender, reciever) = watch::channel(SyncProgress::FindingImages); + self.status_reciever = reciever; + let mut sender_op = Some(sender); + let settings = self.settings.clone(); + let users = users.clone(); + self.rt.spawn_blocking(move || { + let task = download_images(&settings, &users, &mut sender_op); + block_on(task); + let _ = sender_op.unwrap().send(SyncProgress::Done); + }); + } + } + UserAction::RefreshImages => { + let (_, reciever) = watch::channel(SyncProgress::NotStarted); + let user = self.image_selected_state.steam_user.clone(); + if let Some(user) = &user { + load_image_grids(user, &mut self.image_selected_state, ui); + } + self.status_reciever = reciever; + } + }; + } + + fn handle_image_type_clear(&mut self, image_type: ImageType) { + let data_folder = &self + .image_selected_state + .steam_user + .as_ref() + .unwrap() + .steam_user_data_folder; + for ext in POSSIBLE_EXTENSIONS { + let file_name = image_type.file_name( + self.image_selected_state + .selected_shortcut + .as_ref() + .unwrap() + .app_id(), + ext, + ); + let path = Path::new(data_folder) + .join("config") + .join("grid") + .join(&file_name); + if path.exists() { + let _ = std::fs::remove_file(&path); + } + let key = path.to_string_lossy().to_string(); + self.image_selected_state.image_handles.remove(&key); + } + self.image_selected_state.image_type_selected = None; + } + + fn handle_correct_grid_request(&mut self) { + let app_name = self + .image_selected_state + .selected_shortcut + .as_ref() + .map(|s| s.name()) + .unwrap_or_default(); + let auth_key = self + .settings + .steamgrid_db + .auth_key + .clone() + .unwrap_or_default(); + let client = steamgriddb_api::Client::new(&auth_key); + let search_results = self.rt.block_on(client.search(app_name)); + 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; + if let Some(auth_key) = &self.settings.steamgrid_db.auth_key { + 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.name(), grid_id); + cache.save(); + } + } + } + + fn handle_user_selected(&mut self, user: SteamUsersInfo, ui: &mut egui::Ui) { + let state = &mut self.image_selected_state; + let shortcuts = load_image_grids(&user, state, ui); + state.user_shortcuts = Some(shortcuts); + state.steam_user = Some(user); + } + + fn handle_image_type_selected(&mut self, image_type: ImageType) { + let state = &mut self.image_selected_state; + state.image_type_selected = Some(image_type); + let (tx, rx) = watch::channel(FetcStatus::Fetching); + self.image_selected_state.image_options = rx; + let settings = self.settings.clone(); + if let Some(auth_key) = settings.steamgrid_db.auth_key { + if let Some(grid_id) = self.image_selected_state.grid_id { + let auth_key = auth_key; + let image_type = image_type; + self.rt.spawn_blocking(move || { + let thumbnails_folder = get_thumbnails_folder(); + let client = steamgriddb_api::Client::new(auth_key); + let query = + get_query_type(false, &image_type, settings.steamgrid_db.allow_nsfw); + let search_res = block_on(client.get_images_for_id(grid_id, &query)); + if let Ok(possible_images) = search_res { + let mut result = vec![]; + for possible_image in &possible_images { + let ext = get_image_extension(&possible_image.mime); + let path = + thumbnails_folder.join(format!("{}.{}", possible_image.id, ext)); + result.push(PossibleImage { + thumbnail_path: path, + mime: possible_image.mime.clone(), + thumbnail_url: possible_image.thumb.clone(), + full_url: possible_image.url.clone(), + }); + } + let _ = tx.send(FetcStatus::Fetched(result)); + } + }); + } + }; + } + + fn handle_image_selected(&mut self, image: PossibleImage) { + //We must have a user here + let user = self.image_selected_state.steam_user.as_ref().unwrap(); + let selected_image_type = self + .image_selected_state + .image_type_selected + .as_ref() + .unwrap(); + let selected_shortcut = self + .image_selected_state + .selected_shortcut + .as_ref() + .unwrap(); + + let ext = get_image_extension(&image.mime); + let to_download_to_path = Path::new(&user.steam_user_data_folder) + .join("config") + .join("grid") + .join(selected_image_type.file_name(selected_shortcut.app_id(), ext)); + + //Delete old possible images + + let data_folder = Path::new(&user.steam_user_data_folder); + + //Keep deleting images of this type untill we don't find any more + let mut path = self.get_shortcut_image_path(data_folder); + while Path::new(&path).exists() { + let _ = std::fs::remove_file(&path); + path = self.get_shortcut_image_path(data_folder); + } + + //Put the loaded thumbnail into the image handler map, we can use that for preview + let full_image_key = to_download_to_path.to_string_lossy().to_string(); + let _ = self + .image_selected_state + .image_handles + .remove(&full_image_key); + let thumbnail_key = image.thumbnail_path.to_string_lossy().to_string(); + let thumbnail = self + .image_selected_state + .image_handles + .remove(&thumbnail_key); + if let Some((_key, thumbnail)) = thumbnail { + self.image_selected_state + .image_handles + .insert(full_image_key, thumbnail); + } + + let app_name = selected_shortcut.name(); + let to_download = ToDownload { + path: to_download_to_path, + url: image.full_url.clone(), + app_name: app_name.to_string(), + image_type: *selected_image_type, + }; + self.rt.spawn_blocking(move || { + let _ = block_on(crate::steamgriddb::download_to_download(&to_download)); + }); + + self.clear_loaded_images(); + { + self.image_selected_state.image_type_selected = None; + self.image_selected_state.image_options = watch::channel(FetcStatus::NeedsFetched).1; + } + } + + fn get_shortcut_image_path(&self, data_folder: &Path) -> String { + self.image_selected_state + .selected_shortcut + .as_ref() + .unwrap() + .key( + &self.image_selected_state.image_type_selected.unwrap(), + data_folder, + ) + .1 + } + + fn clear_loaded_images(&mut self) { + if let FetcStatus::Fetched(options) = &*self.image_selected_state.image_options.borrow() { + for option in options { + let key = option.thumbnail_path.to_string_lossy().to_string(); + self.image_selected_state.image_handles.remove(&key); + } + } + } + + 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(); + if let Some(auth_key) = &self.settings.steamgrid_db.auth_key { + let client = steamgriddb_api::Client::new(auth_key); + let search = CachedSearch::new(&client); + state.grid_id = self + .rt + .block_on(search.search(shortcut.app_id(), shortcut.name())) + .ok() + .flatten(); + } + state.selected_shortcut = Some(shortcut.clone()); + + for image_type in ImageType::all() { + let (path, key) = shortcut.key(image_type, Path::new(&user.steam_user_data_folder)); + let image = load_image_from_path(&path); + if let Ok(image) = image { + let texture = ui + .ctx() + .load_texture(&key, image, egui::TextureOptions::LINEAR); + state + .image_handles + .insert(key, TextureDownloadState::Loaded(texture)); + } + } + state.selected_shortcut = Some(shortcut); + } + + fn handle_back_button_action(&mut self) { + let state = &mut self.image_selected_state; + if state.possible_names.is_some() { + state.possible_names = None; + } else if state.image_type_selected.is_some() { + state.image_type_selected = None; + } else if state.selected_shortcut.is_some() { + state.selected_shortcut = None; + } else { + state.image_handles.clear(); + state.user_shortcuts = None; + state.steam_user = None; + } + } +} + +fn load_image_grids( + user: &SteamUsersInfo, + state: &mut ImageSelectState, + ui: &mut egui::Ui, +) -> Vec { + let user_info = crate::steam::get_shortcuts_for_user(user); + let mut user_folder = user_info.path.clone(); + user_folder.pop(); + user_folder.pop(); + let mut shortcuts = user_info.shortcuts; + shortcuts.sort_by_key(|s| s.app_name.clone()); + let image_type = &ImageType::Grid; + for shortcut in &shortcuts { + let (path, key) = shortcut.key(image_type, &user_folder); + let loaded = state.image_handles.contains_key(&key); + if !loaded && path.exists() { + let image = load_image_from_path(&path); + if let Ok(image) = image { + let texture = ui + .ctx() + .load_texture(&key, image, egui::TextureOptions::LINEAR); + state + .image_handles + .insert(key, TextureDownloadState::Loaded(texture)); + } + } + } + shortcuts +} + +fn render_shortcut_mode_select(state: &ImageSelectState, ui: &mut egui::Ui) -> Option { + let mode_before = state.game_mode.clone(); + let combo_box = egui::ComboBox::new("ImageModeSelect", "").selected_text(mode_before.label()); + let mut mode_after = state.game_mode.clone(); + combo_box.show_ui(ui, |ui| { + ui.selectable_value( + &mut mode_after, + GameMode::Shortcuts, + GameMode::Shortcuts.label(), + ); + ui.selectable_value( + &mut mode_after, + GameMode::SteamGames, + GameMode::SteamGames.label(), + ); + }); + if !mode_after.eq(&mode_before) { + return Some(UserAction::SetGamesMode(mode_after)); + } + None +} + + +fn render_user_select(state: &ImageSelectState, ui: &mut egui::Ui) -> Option { + if state.steam_user.is_none() { + if let Some(users) = &state.steam_users { + if users.len() > 0 { + return Some(UserAction::UserSelected(users[0].clone())); + } + } + } else { + let mut selected_user = state.steam_user.as_ref().unwrap().clone(); + let id_before = selected_user.user_id.clone(); + if let Some(steam_users) = &state.steam_users { + if steam_users.len() > 0 { + let combo_box = egui::ComboBox::new("ImageUserSelect", "") + .selected_text(format!("Steam user id: {}", &selected_user.user_id)); + combo_box.show_ui(ui, |ui| { + for user in steam_users { + ui.selectable_value(&mut selected_user, user.clone(), &user.user_id); + } + }); + } + } + let id_now = selected_user.user_id.clone(); + if !id_before.eq(&id_now) { + return Some(UserAction::UserSelected(selected_user.clone())); + } + } + + None +} + +const MAX_WIDTH: f32 = 300.; \ No newline at end of file diff --git a/src/ui/images/useraction.rs b/src/ui/images/useraction.rs new file mode 100644 index 0000000..00ca57c --- /dev/null +++ b/src/ui/images/useraction.rs @@ -0,0 +1,21 @@ +use crate::{steam::SteamUsersInfo, steamgriddb::ImageType}; + +use super::{gametype::GameType, possible_image::PossibleImage, gamemode::GameMode}; + + +#[derive(Debug)] +pub enum UserAction { + CorrectGridId, + UserSelected(SteamUsersInfo), + ShortcutSelected(GameType), + ImageTypeSelected(ImageType), + ImageTypeCleared(ImageType, bool), + ImageSelected(PossibleImage), + GridIdChanged(usize), + SetGamesMode(GameMode), + BackButton, + NoAction, + ClearImages, + DownloadAllImages, + RefreshImages, +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 67e17f6..234f6b6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,15 +1,14 @@ mod defines; mod ui_backup; mod ui_disconnect; -mod ui_image_download; mod ui_import_games; mod ui_settings; mod uiapp; +mod images; pub use defines::*; pub use ui_backup::*; pub use ui_disconnect::*; -pub use ui_image_download::*; pub use ui_import_games::*; pub use ui_settings::*; pub use uiapp::*; diff --git a/src/ui/ui_image_download.rs b/src/ui/ui_image_download.rs deleted file mode 100644 index 582a5f3..0000000 --- a/src/ui/ui_image_download.rs +++ /dev/null @@ -1,1086 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use crate::{ - config::get_thumbnails_folder, - steam::{get_installed_games, SteamGameInfo}, - steam::{get_shortcuts_paths, SteamUsersInfo}, - steamgriddb::{get_image_extension, get_query_type, CachedSearch, ImageType, ToDownload}, - sync::{download_images, SyncProgress}, -}; -use dashmap::DashMap; -use egui::{Button, Grid, ImageButton, ScrollArea}; -use futures::executor::block_on; -use steam_shortcuts_util::shortcut::ShortcutOwned; -use steamgriddb_api::images::MimeTypes; -use tokio::sync::watch::{self, Receiver}; - -use super::{ui_images::load_image_from_path, FetcStatus, MyEguiApp}; - -pub struct ImageSelectState { - pub selected_shortcut: Option, - pub grid_id: Option, - - pub steam_user: Option, - pub settings_error: 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>, -} - -#[derive(Clone)] -pub enum TextureState { - Downloading, - Downloaded, - Loaded(egui::TextureHandle), - Failed, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum GameMode { - Shortcuts, - SteamGames, -} - -impl GameMode { - pub fn is_shortcuts(&self) -> bool { - match self { - GameMode::Shortcuts => true, - GameMode::SteamGames => false, - } - } - - pub fn label(&self) -> &str { - match self { - GameMode::Shortcuts => "Images for shortcuts", - GameMode::SteamGames => "Images for steam games", - } - } -} - -#[derive(Clone, Debug)] -pub struct PossibleImage { - thumbnail_path: PathBuf, - thumbnail_url: String, - mime: MimeTypes, - full_url: String, -} - -impl Default for ImageSelectState { - fn default() -> Self { - Self { - selected_shortcut: Default::default(), - grid_id: Default::default(), - steam_user: Default::default(), - steam_users: Default::default(), - settings_error: 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(), - } - } -} - -#[derive(Debug)] -enum UserAction { - CorrectGridId, - UserSelected(SteamUsersInfo), - ShortcutSelected(GameType), - ImageTypeSelected(ImageType), - ImageTypeCleared(ImageType, bool), - ImageSelected(PossibleImage), - GridIdChanged(usize), - SetGamesMode(GameMode), - BackButton, - NoAction, - ClearImages, - DownloadAllImages, - RefreshImages, -} - -impl MyEguiApp { - fn render_ui_image_action(&self, ui: &mut egui::Ui) -> UserAction { - let state = &self.image_selected_state; - if let Some(action) = ui - .horizontal(|ui| { - let render_back = - state.selected_shortcut.is_some() || state.image_type_selected.is_some(); - if render_back { - if ui.button("Back").clicked() { - return Some(UserAction::BackButton); - } - None - } else { - if let Some(value) = render_user_select(state, ui) { - return Some(value); - } - render_shortcut_mode_select(state, ui) - } - }) - .inner - { - return action; - } - - if let Some(shortcut) = state.selected_shortcut.as_ref() { - ui.heading(shortcut.name()); - - if let Some(possible_names) = state.possible_names.as_ref() { - if let Some(value) = render_possible_names(possible_names, ui, state) { - return value; - } - } else if let Some(image_type) = state.image_type_selected.as_ref() { - if let Some(action) = self.render_possible_images(ui, image_type, state) { - return action; - } - } else if let Some(action) = render_shortcut_images(ui, state) { - return action; - } - } else { - let is_shortcut = state.game_mode.is_shortcuts(); - 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; - } - - match *self.status_reciever.borrow() { - crate::sync::SyncProgress::FindingImages => { - ui.spinner(); - ui.label("Finding images to download"); - ui.ctx().request_repaint(); - } - crate::sync::SyncProgress::DownloadingImages { to_download } => { - ui.spinner(); - ui.label(format!("Downloading {to_download} images")); - ui.ctx().request_repaint(); - } - crate::sync::SyncProgress::Done => { - ui.ctx().request_repaint(); - return UserAction::RefreshImages; - } - _ => { - if ui.button("Download images for all games").clicked() { - return UserAction::DownloadAllImages; - } - } - } - } - - UserAction::NoAction - } - - fn render_shortcut_select(&self, ui: &mut egui::Ui) -> Option { - let shortcuts = &self.image_selected_state.user_shortcuts; - - let width = ui.available_size().x; - let column_width = 100.; - let column_padding = 23.; - let columns = (width / (column_width + column_padding)).floor() as u32; - let mut cur_column = 0; - match shortcuts { - Some(shortcuts) => { - let user_info = &self.image_selected_state.steam_user.as_ref().unwrap(); - if let Some(action) = egui::Grid::new("ui_images") - .show(ui, |ui| { - for shortcut in shortcuts { - let action = self.render_image(shortcut, user_info, column_width, ui); - if action.is_some() { - return action; - } - cur_column += 1; - if cur_column >= columns { - cur_column = 0; - ui.end_row(); - } - } - ui.end_row(); - None - }) - .inner - { - return action; - } - } - None => { - ui.label("Could not find any shortcuts"); - } - } - None - } - - fn render_image( - &self, - shortcut: &ShortcutOwned, - user_info: &&SteamUsersInfo, - column_width: f32, - ui: &mut egui::Ui, - ) -> Option> { - let (_, key) = shortcut.key( - &ImageType::Grid, - Path::new(&user_info.steam_user_data_folder), - ); - let mut clicked = false; - - let texture = self.image_selected_state.image_handles.get(&key); - if let Some(texture) = texture { - if let TextureState::Loaded(texture) = &texture.value() { - let mut size = texture.size_vec2(); - clamp_to_width(&mut size, column_width); - let image_button = ImageButton::new(texture, size); - clicked = ui - .add(image_button) - .on_hover_text(&shortcut.app_name) - .clicked(); - } - } else { - let button = ui.add_sized( - [column_width, column_width * 1.6], - Button::new(&shortcut.app_name).wrap(true), - ); - clicked = clicked || button.clicked(); - } - - if clicked { - return Some(Some(UserAction::ShortcutSelected(GameType::Shortcut( - shortcut.clone(), - )))); - } - None - } - - fn render_possible_images( - &self, - ui: &mut egui::Ui, - image_type: &ImageType, - state: &ImageSelectState, - ) -> Option { - ui.label(image_type.name()); - - if let Some(action) = ui - .horizontal(|ui| { - if ui - .small_button("Clear image?") - .on_hover_text("Click here to clear the image") - .clicked() - { - return Some(UserAction::ImageTypeCleared(*image_type, false)); - } - - if ui - .small_button("Stop downloading this image?") - .on_hover_text("Stop downloading this type of image for this shortcut at all") - .clicked() - { - return Some(UserAction::ImageTypeCleared(*image_type, true)); - } - None - }) - .inner - { - return Some(action); - } - let column_padding = 10.; - let column_width = MAX_WIDTH * 0.75; - let width = ui.available_width(); - let columns = (width / (column_width + column_padding)).floor() as u32; - let mut column = 0; - match &*state.image_options.borrow() { - FetcStatus::Fetched(images) => { - let x = Grid::new("ImageThumbnailSelectGrid") - .spacing([column_padding, column_padding]) - .show(ui, |ui| { - for image in images { - let image_key = - image.thumbnail_path.as_path().to_string_lossy().to_string(); - - match state.image_handles.get_mut(&image_key) { - Some(mut state) => { - match state.value() { - TextureState::Downloading => { - ui.ctx().request_repaint(); - //nothing to do,just wait - ui.spinner(); - } - TextureState::Downloaded => { - //Need to load - let image_data = - load_image_from_path(&image.thumbnail_path); - match image_data { - Ok(image_data) => { - let handle = ui.ctx().load_texture( - &image_key, - image_data, - egui::TextureOptions::LINEAR, - ); - *state.value_mut() = - TextureState::Loaded(handle); - ui.spinner(); - } - Err(_) => *state.value_mut() = TextureState::Failed, - } - ui.ctx().request_repaint(); - } - TextureState::Loaded(texture_handle) => { - //need to show - let mut size = texture_handle.size_vec2(); - clamp_to_width(&mut size, column_width); - let image_button = - ImageButton::new(texture_handle, size); - if ui.add_sized(size, image_button).clicked() { - return Some(UserAction::ImageSelected( - image.clone(), - )); - } - } - TextureState::Failed => { - ui.label("Failed to load image"); - } - } - } - None => { - //We need to start a download - let image_handles = &self.image_selected_state.image_handles; - let path = &image.thumbnail_path; - //Redownload if file is too small - if !path.exists() - || std::fs::metadata(path) - .map(|m| m.len()) - .unwrap_or_default() - < 2 - { - image_handles - .insert(image_key.clone(), TextureState::Downloading); - let to_download = ToDownload { - path: path.clone(), - url: image.thumbnail_url.clone(), - app_name: "Thumbnail".to_string(), - image_type: *image_type, - }; - let image_handles = image_handles.clone(); - let image_key = image_key.clone(); - self.rt.spawn_blocking(move || { - block_on(crate::steamgriddb::download_to_download( - &to_download, - )) - .unwrap(); - image_handles - .insert(image_key, TextureState::Downloaded); - }); - } else { - image_handles - .insert(image_key.clone(), TextureState::Downloaded); - } - } - } - column += 1; - if column >= columns { - column = 0; - ui.end_row(); - } - } - - None - }) - .inner; - if x.is_some() { - return x; - } - } - _ => { - ui.horizontal(|ui| { - ui.spinner(); - ui.label("Finding possible images"); - }); - ui.ctx().request_repaint(); - } - } - None - } - - fn ensure_steam_users_loaded(&mut self) { - if self.image_selected_state.settings_error.is_none() - && self.image_selected_state.steam_users.is_none() - { - let paths = get_shortcuts_paths(&self.settings.steam); - match paths { - Ok(paths) => self.image_selected_state.steam_users = Some(paths), - Err(err) => { - self.image_selected_state.settings_error = Some(format!("Could not find user steam location, error message: {} , try to clear the steam location field in settings to let BoilR find it itself",err)); - } - } - } - } - - pub(crate) fn render_ui_images(&mut self, ui: &mut egui::Ui) { - self.ensure_steam_users_loaded(); - - if let Some(error_message) = &self.image_selected_state.settings_error { - ui.label(error_message); - return; - } - - let mut action = UserAction::NoAction; - ScrollArea::vertical() - .stick_to_right(true) - .auto_shrink([false, true]) - .show(ui, |ui| { - ui.reset_style(); - action = self.render_ui_image_action(ui); - }); - match action { - UserAction::UserSelected(user) => { - self.handle_user_selected(user, ui); - } - UserAction::ShortcutSelected(shortcut) => { - self.handle_shortcut_selected(shortcut, ui); - } - UserAction::ImageTypeSelected(image_type) => { - self.handle_image_type_selected(image_type); - } - UserAction::ImageSelected(image) => { - self.handle_image_selected(image); - } - UserAction::BackButton => { - self.handle_back_button_action(); - } - 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(); - } - UserAction::ImageTypeCleared(image_type, should_ban) => { - let app_id = self - .image_selected_state - .selected_shortcut - .as_ref() - .unwrap() - .app_id(); - self.settings - .steamgrid_db - .set_image_banned(&image_type, app_id, should_ban); - self.handle_image_type_clear(image_type); - } - UserAction::ClearImages => { - for image_type in ImageType::all() { - self.handle_image_type_clear(*image_type); - } - self.handle_back_button_action(); - } - UserAction::DownloadAllImages => { - if let Some(users) = &self.image_selected_state.steam_users { - let (sender, reciever) = watch::channel(SyncProgress::FindingImages); - self.status_reciever = reciever; - let mut sender_op = Some(sender); - let settings = self.settings.clone(); - let users = users.clone(); - self.rt.spawn_blocking(move || { - let task = download_images(&settings, &users, &mut sender_op); - block_on(task); - let _ = sender_op.unwrap().send(SyncProgress::Done); - }); - } - } - UserAction::RefreshImages => { - let (_, reciever) = watch::channel(SyncProgress::NotStarted); - let user = self.image_selected_state.steam_user.clone(); - if let Some(user) = &user { - load_image_grids(user, &mut self.image_selected_state, ui); - } - self.status_reciever = reciever; - } - }; - } - - fn handle_image_type_clear(&mut self, image_type: ImageType) { - let data_folder = &self - .image_selected_state - .steam_user - .as_ref() - .unwrap() - .steam_user_data_folder; - for ext in POSSIBLE_EXTENSIONS { - let file_name = image_type.file_name( - self.image_selected_state - .selected_shortcut - .as_ref() - .unwrap() - .app_id(), - ext, - ); - let path = Path::new(data_folder) - .join("config") - .join("grid") - .join(&file_name); - if path.exists() { - let _ = std::fs::remove_file(&path); - } - let key = path.to_string_lossy().to_string(); - self.image_selected_state.image_handles.remove(&key); - } - self.image_selected_state.image_type_selected = None; - } - - fn handle_correct_grid_request(&mut self) { - let app_name = self - .image_selected_state - .selected_shortcut - .as_ref() - .map(|s| s.name()) - .unwrap_or_default(); - let auth_key = self - .settings - .steamgrid_db - .auth_key - .clone() - .unwrap_or_default(); - let client = steamgriddb_api::Client::new(&auth_key); - let search_results = self.rt.block_on(client.search(app_name)); - 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; - if let Some(auth_key) = &self.settings.steamgrid_db.auth_key { - 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.name(), grid_id); - cache.save(); - } - } - } - - fn handle_user_selected(&mut self, user: SteamUsersInfo, ui: &mut egui::Ui) { - let state = &mut self.image_selected_state; - let shortcuts = load_image_grids(&user, state, ui); - state.user_shortcuts = Some(shortcuts); - state.steam_user = Some(user); - } - - fn handle_image_type_selected(&mut self, image_type: ImageType) { - let state = &mut self.image_selected_state; - state.image_type_selected = Some(image_type); - let (tx, rx) = watch::channel(FetcStatus::Fetching); - self.image_selected_state.image_options = rx; - let settings = self.settings.clone(); - if let Some(auth_key) = settings.steamgrid_db.auth_key { - if let Some(grid_id) = self.image_selected_state.grid_id { - let auth_key = auth_key; - let image_type = image_type; - self.rt.spawn_blocking(move || { - let thumbnails_folder = get_thumbnails_folder(); - let client = steamgriddb_api::Client::new(auth_key); - let query = - get_query_type(false, &image_type, settings.steamgrid_db.allow_nsfw); - let search_res = block_on(client.get_images_for_id(grid_id, &query)); - if let Ok(possible_images) = search_res { - let mut result = vec![]; - for possible_image in &possible_images { - let ext = get_image_extension(&possible_image.mime); - let path = - thumbnails_folder.join(format!("{}.{}", possible_image.id, ext)); - result.push(PossibleImage { - thumbnail_path: path, - mime: possible_image.mime.clone(), - thumbnail_url: possible_image.thumb.clone(), - full_url: possible_image.url.clone(), - }); - } - let _ = tx.send(FetcStatus::Fetched(result)); - } - }); - } - }; - } - - fn handle_image_selected(&mut self, image: PossibleImage) { - //We must have a user here - let user = self.image_selected_state.steam_user.as_ref().unwrap(); - let selected_image_type = self - .image_selected_state - .image_type_selected - .as_ref() - .unwrap(); - let selected_shortcut = self - .image_selected_state - .selected_shortcut - .as_ref() - .unwrap(); - - let ext = get_image_extension(&image.mime); - let to_download_to_path = Path::new(&user.steam_user_data_folder) - .join("config") - .join("grid") - .join(selected_image_type.file_name(selected_shortcut.app_id(), ext)); - - //Delete old possible images - - let data_folder = Path::new(&user.steam_user_data_folder); - - //Keep deleting images of this type untill we don't find any more - let mut path = self.get_shortcut_image_path(data_folder); - while Path::new(&path).exists() { - let _ = std::fs::remove_file(&path); - path = self.get_shortcut_image_path(data_folder); - } - - //Put the loaded thumbnail into the image handler map, we can use that for preview - let full_image_key = to_download_to_path.to_string_lossy().to_string(); - let _ = self - .image_selected_state - .image_handles - .remove(&full_image_key); - let thumbnail_key = image.thumbnail_path.to_string_lossy().to_string(); - let thumbnail = self - .image_selected_state - .image_handles - .remove(&thumbnail_key); - if let Some((_key, thumbnail)) = thumbnail { - self.image_selected_state - .image_handles - .insert(full_image_key, thumbnail); - } - - let app_name = selected_shortcut.name(); - let to_download = ToDownload { - path: to_download_to_path, - url: image.full_url.clone(), - app_name: app_name.to_string(), - image_type: *selected_image_type, - }; - self.rt.spawn_blocking(move || { - let _ = block_on(crate::steamgriddb::download_to_download(&to_download)); - }); - - self.clear_loaded_images(); - { - self.image_selected_state.image_type_selected = None; - self.image_selected_state.image_options = watch::channel(FetcStatus::NeedsFetched).1; - } - } - - fn get_shortcut_image_path(&self, data_folder: &Path) -> String { - self.image_selected_state - .selected_shortcut - .as_ref() - .unwrap() - .key( - &self.image_selected_state.image_type_selected.unwrap(), - data_folder, - ) - .1 - } - - fn clear_loaded_images(&mut self) { - if let FetcStatus::Fetched(options) = &*self.image_selected_state.image_options.borrow() { - for option in options { - let key = option.thumbnail_path.to_string_lossy().to_string(); - self.image_selected_state.image_handles.remove(&key); - } - } - } - - 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(); - if let Some(auth_key) = &self.settings.steamgrid_db.auth_key { - let client = steamgriddb_api::Client::new(auth_key); - let search = CachedSearch::new(&client); - state.grid_id = self - .rt - .block_on(search.search(shortcut.app_id(), shortcut.name())) - .ok() - .flatten(); - } - state.selected_shortcut = Some(shortcut.clone()); - - for image_type in ImageType::all() { - let (path, key) = shortcut.key(image_type, Path::new(&user.steam_user_data_folder)); - let image = load_image_from_path(&path); - if let Ok(image) = image { - let texture = ui - .ctx() - .load_texture(&key, image, egui::TextureOptions::LINEAR); - state - .image_handles - .insert(key, TextureState::Loaded(texture)); - } - } - state.selected_shortcut = Some(shortcut); - } - - fn handle_back_button_action(&mut self) { - let state = &mut self.image_selected_state; - if state.possible_names.is_some() { - state.possible_names = None; - } else if state.image_type_selected.is_some() { - state.image_type_selected = None; - } else if state.selected_shortcut.is_some() { - state.selected_shortcut = None; - } else { - state.image_handles.clear(); - state.user_shortcuts = None; - state.steam_user = None; - } - } -} - -fn load_image_grids( - user: &SteamUsersInfo, - state: &mut ImageSelectState, - ui: &mut egui::Ui, -) -> Vec { - let user_info = crate::steam::get_shortcuts_for_user(user); - let mut user_folder = user_info.path.clone(); - user_folder.pop(); - user_folder.pop(); - let mut shortcuts = user_info.shortcuts; - shortcuts.sort_by_key(|s| s.app_name.clone()); - let image_type = &ImageType::Grid; - for shortcut in &shortcuts { - let (path, key) = shortcut.key(image_type, &user_folder); - let loaded = state.image_handles.contains_key(&key); - if !loaded && path.exists() { - let image = load_image_from_path(&path); - if let Ok(image) = image { - let texture = ui - .ctx() - .load_texture(&key, image, egui::TextureOptions::LINEAR); - state - .image_handles - .insert(key, TextureState::Loaded(texture)); - } - } - } - shortcuts -} - -fn render_shortcut_mode_select(state: &ImageSelectState, ui: &mut egui::Ui) -> Option { - let mode_before = state.game_mode.clone(); - let combo_box = egui::ComboBox::new("ImageModeSelect", "").selected_text(mode_before.label()); - let mut mode_after = state.game_mode.clone(); - combo_box.show_ui(ui, |ui| { - ui.selectable_value( - &mut mode_after, - GameMode::Shortcuts, - GameMode::Shortcuts.label(), - ); - ui.selectable_value( - &mut mode_after, - GameMode::SteamGames, - GameMode::SteamGames.label(), - ); - }); - if !mode_after.eq(&mode_before) { - return Some(UserAction::SetGamesMode(mode_after)); - } - None -} - -fn render_possible_names( - possible_names: &Vec, - ui: &mut egui::Ui, - state: &ImageSelectState, -) -> Option { - let mut grid_id_text = state.grid_id.map(|id| id.to_string()).unwrap_or_default(); - ui.label("SteamGridDB ID") - .on_hover_text("You can change this id to one you have found at the steamgriddb webpage"); - if ui.text_edit_singleline(&mut grid_id_text).changed() { - if let Ok(grid_id) = grid_id_text.parse::() { - return Some(UserAction::GridIdChanged(grid_id)); - } - }; - - for possible in possible_names { - if ui.button(&possible.name).clicked() { - return Some(UserAction::GridIdChanged(possible.id)); - } - } - - ui.separator(); - if ui - .button("Clear all images") - .on_hover_text("Clicking this deletes all images for this shortcut") - .clicked() - { - return Some(UserAction::ClearImages); - } - 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 shortcut = state.selected_shortcut.as_ref().unwrap(); - let user_path = &state.steam_user.as_ref().unwrap().steam_user_data_folder; - let x = if ui.available_width() > MAX_WIDTH * 3. { - ui.horizontal(|ui| { - let x = ui - .vertical(|ui| { - let texture = - texture_from_iamge_type(shortcut, &ImageType::Grid, user_path, state); - ui.label(ImageType::Grid.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::Grid)); - } - None - }) - .inner; - if x.is_some() { - return x; - } - let x = ui - .vertical(|ui| { - let texture = - texture_from_iamge_type(shortcut, &ImageType::Hero, user_path, state); - ui.label(ImageType::Hero.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::Hero)); - } - let texture = - texture_from_iamge_type(shortcut, &ImageType::WideGrid, user_path, state); - ui.label(ImageType::WideGrid.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::WideGrid)); - } - - let texture = - texture_from_iamge_type(shortcut, &ImageType::Logo, user_path, state); - ui.label(ImageType::Logo.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::Logo)); - } - None - }) - .inner; - if x.is_some() { - return x; - } - ui.vertical(|ui| { - let texture = texture_from_iamge_type(shortcut, &ImageType::Icon, user_path, state); - ui.label(ImageType::Icon.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::Icon)); - } - - let texture = - texture_from_iamge_type(shortcut, &ImageType::BigPicture, user_path, state); - ui.label(ImageType::BigPicture.name()); - if render_thumbnail(ui, texture).clicked() { - return Some(UserAction::ImageTypeSelected(ImageType::BigPicture)); - } - None - }) - .inner - }) - .inner - } else { - render_image_types_as_list(shortcut, user_path, state, ui) - }; - - if ui - .button("Click here if the images are for a wrong game") - .clicked() - { - return Some(UserAction::CorrectGridId); - } - x -} - -fn render_image_types_as_list( - shortcut: &GameType, - user_path: &String, - state: &ImageSelectState, - ui: &mut egui::Ui, -) -> Option { - let types = ImageType::all(); - for image_type in types { - let texture = texture_from_iamge_type(shortcut, image_type, user_path, state); - let response = ui - .vertical(|ui| { - ui.label(image_type.name()); - render_thumbnail(ui, texture) - }) - .inner; - if response.clicked() { - return Some(UserAction::ImageTypeSelected(*image_type)); - } - } - None -} - -fn texture_from_iamge_type( - shortcut: &GameType, - image_type: &ImageType, - user_path: &String, - state: &ImageSelectState, -) -> Option { - let (_path, key) = shortcut.key(image_type, Path::new(&user_path)); - state.image_handles.get(&key).and_then(|k| match k.value() { - TextureState::Loaded(texture) => Some(texture.clone()), - _ => None, - }) -} - -fn render_user_select(state: &ImageSelectState, ui: &mut egui::Ui) -> Option { - if state.steam_user.is_none() { - if let Some(users) = &state.steam_users { - if users.len() > 0 { - return Some(UserAction::UserSelected(users[0].clone())); - } - } - } else { - let mut selected_user = state.steam_user.as_ref().unwrap().clone(); - let id_before = selected_user.user_id.clone(); - if let Some(steam_users) = &state.steam_users { - if steam_users.len() > 0 { - let combo_box = egui::ComboBox::new("ImageUserSelect", "") - .selected_text(format!("Steam user id: {}", &selected_user.user_id)); - combo_box.show_ui(ui, |ui| { - for user in steam_users { - ui.selectable_value(&mut selected_user, user.clone(), &user.user_id); - } - }); - } - } - let id_now = selected_user.user_id.clone(); - if !id_before.eq(&id_now) { - return Some(UserAction::UserSelected(selected_user.clone())); - } - } - - None -} - -const MAX_WIDTH: f32 = 300.; - -fn render_thumbnail(ui: &mut egui::Ui, image: Option) -> egui::Response { - if let Some(texture) = image { - let mut size = texture.size_vec2(); - clamp_to_width(&mut size, MAX_WIDTH); - let image_button = ImageButton::new(&texture, size); - ui.add(image_button) - } else { - ui.button("Pick an image") - } -} - -fn clamp_to_width(size: &mut egui::Vec2, max_width: f32) { - let mut x = size.x; - let mut y = size.y; - if size.x > max_width { - let ratio = size.y / size.x; - x = max_width; - y = x * ratio; - } - size.x = x; - size.y = y; -} - -trait HasImageKey { - fn key(&self, image_type: &ImageType, user_path: &Path) -> (PathBuf, String); -} -const POSSIBLE_EXTENSIONS: [&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.find(|(exsists, _, _)| *exsists); - 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.app_id, image_type, user_path, ext)); - let first = keys.next().unwrap(); - let other = keys.find(|(exsists, _, _)| *exsists); - let (_, path, key) = other.unwrap_or(first); - (path, key) - } -} - -fn key_from_extension( - app_id: u32, - image_type: &ImageType, - user_path: &Path, - ext: &str, -) -> (bool, PathBuf, String) { - 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) -} diff --git a/src/ui/uiapp.rs b/src/ui/uiapp.rs index e842d50..96b76a2 100644 --- a/src/ui/uiapp.rs +++ b/src/ui/uiapp.rs @@ -21,7 +21,7 @@ use super::{ }, ui_images::{get_import_image, get_logo, get_logo_icon, get_save_image}, ui_import_games::FetcStatus, - BackupState, DiconnectState, ImageSelectState, + BackupState, DiconnectState, images::ImageSelectState, }; const SECTION_SPACING: f32 = 25.0;