From 90c0185c8f7a16e0e34a8202d22fbb5267a4c8e5 Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Fri, 30 Dec 2022 09:22:33 +0100 Subject: [PATCH] Pick image component (#295) * Extract render_possible_image into function * Seperate render_image into reusable functions * Seperate game image into component * Use image component in shortcut overview * Reuse image pick component in type select * Reuse component across select_image_type * Use image type to scale image buttons Use image type to scale * Remove old functions --- src/steamgriddb/image_type.rs | 11 ++ src/ui/components/game_image_button.rs | 147 ++++++++++++++++++ src/ui/components/mod.rs | 6 +- src/ui/images/image_select_state.rs | 5 +- src/ui/images/mod.rs | 5 +- src/ui/images/pages/pick_new_image.rs | 132 ++++------------ src/ui/images/pages/select_image_type.rs | 136 ++++++---------- .../images/pages/shortcut_images_overview.rs | 26 +--- 8 files changed, 251 insertions(+), 217 deletions(-) create mode 100644 src/ui/components/game_image_button.rs diff --git a/src/steamgriddb/image_type.rs b/src/steamgriddb/image_type.rs index cc34736..2ba254c 100644 --- a/src/steamgriddb/image_type.rs +++ b/src/steamgriddb/image_type.rs @@ -22,6 +22,17 @@ impl ImageType { &ALL_TYPES } + pub fn ratio(&self) -> f32{ + match self { + ImageType::Hero => 0.3, + ImageType::Grid => 1.6, + ImageType::WideGrid => 0.5, + ImageType::Logo => 0.2, + ImageType::BigPicture => 0.5, + ImageType::Icon => 1.0, + } + } + pub fn name(&self) -> &str { match self { ImageType::Hero => "Hero", diff --git a/src/ui/components/game_image_button.rs b/src/ui/components/game_image_button.rs new file mode 100644 index 0000000..98746a4 --- /dev/null +++ b/src/ui/components/game_image_button.rs @@ -0,0 +1,147 @@ +use std::path::Path; + +use egui::{Button, ImageButton}; +use futures::executor::block_on; +use tokio::runtime::Runtime; + +use crate::steamgriddb::{ImageType, ToDownload}; +use crate::ui::images::{clamp_to_width, ImageHandles, TextureDownloadState}; +use crate::ui::ui_images::load_image_from_path; + + +pub fn render_image_from_path( + ui: &mut egui::Ui, + image_handles: &ImageHandles, + path: &Path, + max_width: f32, + text: &str, +) -> bool { + render_possible_image(ui, image_handles, path, max_width, text, &ImageType::Grid, None, None) +} + +pub fn render_image_from_path_image_type( + ui: &mut egui::Ui, + image_handles: &ImageHandles, + path: &Path, + max_width: f32, + text: &str, + image_type: &ImageType, +) -> bool { + render_possible_image(ui, image_handles, path, max_width, text, image_type, None, None) +} + + +pub fn render_image_from_path_or_url( + ui: &mut egui::Ui, + image_handles: &ImageHandles, + path: &Path, + max_width: f32, + text: &str, + image_type: &ImageType, + rt: &Runtime, + url: &str, +) -> bool { + render_possible_image( + ui, + image_handles, + path, + max_width, + text, + image_type, + Some(rt), + Some(url), + ) +} + +fn render_possible_image( + ui: &mut egui::Ui, + image_handles: &ImageHandles, + path: &Path, + max_width: f32, + text: &str, + image_type: &ImageType, + rt: Option<&Runtime>, + url: Option<&str>, +) -> bool { + let image_key = path.to_string_lossy().to_string(); + + match 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(&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, max_width); + let image_button = ImageButton::new(texture_handle, size); + if ui + .add_sized(size, image_button) + .on_hover_text(text) + .clicked() + { + return true; + } + } + TextureDownloadState::Failed => { + let button = + ui.add_sized([max_width, max_width * image_type.ratio()], Button::new(text).wrap(true)); + if button.clicked() { + return true; + } + } + } + } + None => { + if !path.exists() && url.is_none() { + image_handles.insert(image_key, TextureDownloadState::Failed); + } else { + //We need to start a download + //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.to_path_buf(), + url: url.unwrap().to_string(), + app_name: "Thumbnail".to_string(), + image_type: *image_type, + }; + let image_handles = image_handles.clone(); + let image_key = image_key.clone(); + if let Some(rt) = rt { + 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); + } + } + } + } + false +} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 214351e..5983c26 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,3 +1,7 @@ mod steam_user_select; +mod game_image_button; -pub use steam_user_select::render_user_select; \ No newline at end of file +pub use steam_user_select::render_user_select; +pub use game_image_button::render_image_from_path; +pub use game_image_button::render_image_from_path_or_url; +pub use game_image_button::render_image_from_path_image_type; \ No newline at end of file diff --git a/src/ui/images/image_select_state.rs b/src/ui/images/image_select_state.rs index ef85be8..9cf34d2 100644 --- a/src/ui/images/image_select_state.rs +++ b/src/ui/images/image_select_state.rs @@ -8,6 +8,9 @@ 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 type ImageHandles = std::sync::Arc>; + pub struct ImageSelectState { pub selected_shortcut: Option, pub grid_id: Option, @@ -20,7 +23,7 @@ pub struct ImageSelectState { pub image_type_selected: Option, pub image_options: Receiver>>, pub steam_games: Option>, - pub image_handles: std::sync::Arc>, + pub image_handles: ImageHandles, pub possible_names: Option>, } diff --git a/src/ui/images/mod.rs b/src/ui/images/mod.rs index 752d3f9..e6166e3 100644 --- a/src/ui/images/mod.rs +++ b/src/ui/images/mod.rs @@ -11,4 +11,7 @@ mod constants; mod pages; -pub use image_select_state::ImageSelectState; \ No newline at end of file +pub use image_select_state::ImageSelectState; +pub use image_select_state::ImageHandles; +pub use texturestate::TextureDownloadState; +pub use image_resize::clamp_to_width; \ 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 index 12466dd..f324498 100644 --- a/src/ui/images/pages/pick_new_image.rs +++ b/src/ui/images/pages/pick_new_image.rs @@ -1,19 +1,20 @@ use std::path::Path; -use egui::{Grid, ImageButton}; +use egui::{Grid, }; use futures::executor::block_on; -use tokio::sync::watch; +use tokio::{ sync::watch}; use crate::{ steamgriddb::{get_image_extension, ImageType, ToDownload}, ui::{ images::{ - constants::MAX_WIDTH, image_resize::clamp_to_width, - image_select_state::ImageSelectState, possible_image::PossibleImage, - texturestate::TextureDownloadState, useraction::UserAction, hasimagekey::HasImageKey, + constants::MAX_WIDTH, + hasimagekey::HasImageKey, + image_select_state::{ ImageSelectState}, + possible_image::PossibleImage, + useraction::UserAction, }, - ui_images::load_image_from_path, - FetcStatus, MyEguiApp, + FetcStatus, MyEguiApp, components::render_image_from_path_or_url, }, }; @@ -59,88 +60,18 @@ pub fn render_page_pick_image( .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, - ); - } - } + let path = image.thumbnail_path.as_path(); + if render_image_from_path_or_url( + ui, + &state.image_handles, + &path, + column_width, + &image.full_url, + image_type, + &app.rt, + &image.thumbnail_url, + ) { + return Some(image.clone()); } column += 1; if column >= columns { @@ -148,13 +79,12 @@ pub fn render_page_pick_image( ui.end_row(); } } - None }) .inner; - if x.is_some() { - return x; - } + if let Some(x) = x { + return Some(UserAction::ImageSelected(x)); + } } _ => { ui.horizontal(|ui| { @@ -188,10 +118,10 @@ pub fn handle_image_selected(app: &mut MyEguiApp, image: PossibleImage) { 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 = get_shortcut_image_path(app,data_folder); + let mut path = get_shortcut_image_path(app, data_folder); while Path::new(&path).exists() { let _ = std::fs::remove_file(&path); - path = get_shortcut_image_path(app,data_folder); + path = get_shortcut_image_path(app, data_folder); } //Put the loaded thumbnail into the image handler map, we can use that for preview @@ -229,7 +159,7 @@ pub fn handle_image_selected(app: &mut MyEguiApp, image: PossibleImage) { } } -fn get_shortcut_image_path(app:&MyEguiApp, data_folder: &Path) -> String { +fn get_shortcut_image_path(app: &MyEguiApp, data_folder: &Path) -> String { app.image_selected_state .selected_shortcut .as_ref() @@ -242,10 +172,10 @@ fn get_shortcut_image_path(app:&MyEguiApp, data_folder: &Path) -> String { } fn clear_loaded_images(app: &mut MyEguiApp) { - if let FetcStatus::Fetched(options) = &*app.image_selected_state.image_options.borrow() { - for option in options { - let key = option.thumbnail_path.to_string_lossy().to_string(); - app.image_selected_state.image_handles.remove(&key); - } + if let FetcStatus::Fetched(options) = &*app.image_selected_state.image_options.borrow() { + for option in options { + let key = option.thumbnail_path.to_string_lossy().to_string(); + app.image_selected_state.image_handles.remove(&key); } } +} diff --git a/src/ui/images/pages/select_image_type.rs b/src/ui/images/pages/select_image_type.rs index 03cb8b1..b43f64d 100644 --- a/src/ui/images/pages/select_image_type.rs +++ b/src/ui/images/pages/select_image_type.rs @@ -1,78 +1,61 @@ 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}; +use crate::ui::images::{ + gametype::GameType, hasimagekey::HasImageKey, image_select_state::ImageSelectState, + useraction::UserAction, ImageHandles, +}; +use crate::{steamgriddb::ImageType, ui::components::render_image_from_path_image_type}; const MAX_WIDTH: f32 = 300.; - -pub fn render_page_shortcut_select_image_type(ui: &mut egui::Ui, state: &ImageSelectState) -> Option { +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 thumbnail = |ui: &mut egui::Ui, image_type: &ImageType| { + if render_thumbnail(ui, &state.image_handles, shortcut, image_type, user_path) { + Some(UserAction::ImageTypeSelected(*image_type)) + } else { + None + } + }; 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; + let x = ui.vertical(|ui| thumbnail(ui, &ImageType::Grid)).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 + let types = &[ImageType::Hero, ImageType::WideGrid, ImageType::Logo]; + types + .iter() + .flat_map(|image_type| thumbnail(ui, image_type)) + .next() }) .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 + let types = &[ImageType::Icon, ImageType::BigPicture]; + types + .iter() + .flat_map(|image_type| thumbnail(ui, image_type)) + .next() }) .inner }) .inner } else { - render_image_types_as_list(shortcut, user_path, state, ui) + let types = ImageType::all(); + types + .iter() + .flat_map(|image_type| thumbnail(ui, image_type)) + .next() }; if ui @@ -84,48 +67,21 @@ pub fn render_page_shortcut_select_image_type(ui: &mut egui::Ui, state: &ImageSe x } -fn render_image_types_as_list( - shortcut: &GameType, - user_path: &String, - state: &ImageSelectState, +fn render_thumbnail( 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( + image_handles: &ImageHandles, 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") - } +) -> bool { + let (path, _key) = shortcut.key(image_type, Path::new(&user_path)); + let text = format!("Pick {} image", image_type.name()); + render_image_from_path_image_type( + ui, + image_handles, + path.as_path(), + MAX_WIDTH, + &text, + image_type, + ) } diff --git a/src/ui/images/pages/shortcut_images_overview.rs b/src/ui/images/pages/shortcut_images_overview.rs index 4da6f79..97c0cb1 100644 --- a/src/ui/images/pages/shortcut_images_overview.rs +++ b/src/ui/images/pages/shortcut_images_overview.rs @@ -1,6 +1,5 @@ use std::path::Path; -use egui::{Button, ImageButton}; use steam_shortcuts_util::shortcut::ShortcutOwned; use crate::{ @@ -8,10 +7,10 @@ use crate::{ steamgriddb::{ImageType, CachedSearch}, ui::{ images::{ - gametype::GameType, hasimagekey::HasImageKey, image_resize::clamp_to_width, + gametype::GameType, hasimagekey::HasImageKey, texturestate::TextureDownloadState, useraction::UserAction, }, - MyEguiApp, ui_images::load_image_from_path, + MyEguiApp, ui_images::load_image_from_path, components::render_image_from_path, }, }; @@ -65,27 +64,8 @@ fn render_image( &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(); - } + let clicked= render_image_from_path(ui, &app.image_selected_state.image_handles, Path::new(&key), column_width, &shortcut.app_name); if clicked { return Some(Some(UserAction::ShortcutSelected(GameType::Shortcut( shortcut.clone(),