mirror of
https://github.com/djibux/BoilR.git
synced 2026-09-01 05:53:41 +02:00
Fix UI blocking in Images tab (#461)
The Images tab would freeze when selecting shortcuts or correcting grid IDs because block_on() was called on the main UI thread. Changes: - Convert handle_shortcut_selected() to spawn async SteamGridDB search in the background instead of blocking the UI thread - Convert handle_correct_grid_request() to spawn async name search in the background instead of blocking - Add poll_grid_id_search() and poll_name_search() to receive async results via watch channels - Add grid_id_search and name_search fields to ImageSelectState - Fix Windows file:// URL paths by normalizing backslashes to forward slashes (file:/// prefix for absolute paths) - Remove blocking thread::sleep(100ms) after image selection, replace with request_repaint() Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
80695c2b95
commit
e890ef940c
@@ -7,6 +7,9 @@ use super::{ gamemode::GameMode, possible_image::PossibleImage, gametype::GameT
|
|||||||
use tokio::sync::watch::{self, Receiver};
|
use tokio::sync::watch::{self, Receiver};
|
||||||
|
|
||||||
|
|
||||||
|
/// Result type for grid ID search operations
|
||||||
|
pub type GridIdSearchResult = Result<Option<usize>, String>;
|
||||||
|
|
||||||
pub struct ImageSelectState {
|
pub struct ImageSelectState {
|
||||||
pub selected_shortcut: Option<GameType>,
|
pub selected_shortcut: Option<GameType>,
|
||||||
pub grid_id: Option<usize>,
|
pub grid_id: Option<usize>,
|
||||||
@@ -21,6 +24,12 @@ pub struct ImageSelectState {
|
|||||||
pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>,
|
pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>,
|
||||||
|
|
||||||
pub possible_names: Option<Vec<steamgriddb_api::search::SearchResult>>,
|
pub possible_names: Option<Vec<steamgriddb_api::search::SearchResult>>,
|
||||||
|
|
||||||
|
/// Receiver for async grid ID search results
|
||||||
|
pub grid_id_search: Receiver<FetchStatus<GridIdSearchResult>>,
|
||||||
|
|
||||||
|
/// Receiver for async name search results (for "correct grid ID" feature)
|
||||||
|
pub name_search: Receiver<FetchStatus<Vec<steamgriddb_api::search::SearchResult>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +48,8 @@ impl Default for ImageSelectState {
|
|||||||
possible_names: None,
|
possible_names: None,
|
||||||
image_options: watch::channel(FetchStatus::NeedsFetched).1,
|
image_options: watch::channel(FetchStatus::NeedsFetched).1,
|
||||||
steam_games: None,
|
steam_games: None,
|
||||||
|
grid_id_search: watch::channel(FetchStatus::NeedsFetched).1,
|
||||||
|
name_search: watch::channel(FetchStatus::NeedsFetched).1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
steamgriddb::CachedSearch,
|
steamgriddb::CachedSearch,
|
||||||
ui::{
|
ui::{
|
||||||
images::{image_select_state::ImageSelectState, useraction::UserAction},
|
images::{image_select_state::ImageSelectState, useraction::UserAction},
|
||||||
MyEguiApp,
|
FetchStatus, MyEguiApp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,20 +53,28 @@ pub fn handle_grid_change(app: &mut MyEguiApp, grid_id: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn handle_correct_grid_request(app:&mut MyEguiApp) {
|
pub fn handle_correct_grid_request(app: &mut MyEguiApp) {
|
||||||
let app_name = app
|
let app_name = app
|
||||||
.image_selected_state
|
.image_selected_state
|
||||||
.selected_shortcut
|
.selected_shortcut
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.name())
|
.map(|s| s.name().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let auth_key = app
|
|
||||||
.settings
|
if let Some(auth_key) = app.settings.steamgrid_db.auth_key.clone() {
|
||||||
.steamgrid_db
|
// Create channel to communicate results
|
||||||
.auth_key
|
let (tx, rx) = watch::channel(FetchStatus::Fetching);
|
||||||
.clone()
|
app.image_selected_state.name_search = rx;
|
||||||
.unwrap_or_default();
|
|
||||||
let client = steamgriddb_api::Client::new(auth_key);
|
// Clear any existing possible_names so we show loading state
|
||||||
let search_results = app.rt.block_on(client.search(app_name));
|
app.image_selected_state.possible_names = None;
|
||||||
app.image_selected_state.possible_names = search_results.ok();
|
|
||||||
|
// Spawn the search in the background instead of blocking the UI
|
||||||
|
app.rt.spawn(async move {
|
||||||
|
let client = steamgriddb_api::Client::new(auth_key);
|
||||||
|
let search_results = client.search(&app_name).await;
|
||||||
|
let results = search_results.unwrap_or_default();
|
||||||
|
let _ = tx.send(FetchStatus::Fetched(results));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ fn render_thumbnail(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
let (_path, key) = shortcut.key(image_type, Path::new(&user_path));
|
let (_path, key) = shortcut.key(image_type, Path::new(&user_path));
|
||||||
let text = format!("Pick {} image", image_type.name());
|
let text = format!("Pick {} image", image_type.name());
|
||||||
let image = egui::Image::new(format!("file://{}", key)).max_width(MAX_WIDTH).shrink_to_fit();
|
// Convert Windows backslashes to forward slashes for file:// URL
|
||||||
|
let key_normalized = key.replace('\\', "/");
|
||||||
|
let image = egui::Image::new(format!("file:///{}", key_normalized)).max_width(MAX_WIDTH).shrink_to_fit();
|
||||||
let calced = image.calc_size(egui::Vec2 { x: MAX_WIDTH, y: f32::INFINITY }, image.size());
|
let calced = image.calc_size(egui::Vec2 { x: MAX_WIDTH, y: f32::INFINITY }, image.size());
|
||||||
let button = ImageButton::new(image);
|
let button = ImageButton::new(image);
|
||||||
ui.add_sized(calced,button).on_hover_text(text).clicked()
|
ui.add_sized(calced,button).on_hover_text(text).clicked()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use egui::ImageButton;
|
use egui::ImageButton;
|
||||||
use steam_shortcuts_util::shortcut::ShortcutOwned;
|
use steam_shortcuts_util::shortcut::ShortcutOwned;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
steam::SteamUsersInfo,
|
steam::SteamUsersInfo,
|
||||||
@@ -11,7 +12,7 @@ use crate::{
|
|||||||
gametype::GameType, hasimagekey::HasImageKey,
|
gametype::GameType, hasimagekey::HasImageKey,
|
||||||
useraction::UserAction,
|
useraction::UserAction,
|
||||||
},
|
},
|
||||||
MyEguiApp,
|
FetchStatus, MyEguiApp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,7 +67,9 @@ fn render_image(
|
|||||||
&ImageType::Grid,
|
&ImageType::Grid,
|
||||||
Path::new(&user_info.steam_user_data_folder),
|
Path::new(&user_info.steam_user_data_folder),
|
||||||
);
|
);
|
||||||
let image = egui::Image::new(format!("file://{}", key)).max_width(column_width).shrink_to_fit();
|
// Convert Windows backslashes to forward slashes for file:// URL
|
||||||
|
let key_normalized = key.replace('\\', "/");
|
||||||
|
let image = egui::Image::new(format!("file:///{}", key_normalized)).max_width(column_width).shrink_to_fit();
|
||||||
let calced = image.calc_size(egui::Vec2 { x: column_width, y: f32::INFINITY }, image.size());
|
let calced = image.calc_size(egui::Vec2 { x: column_width, y: f32::INFINITY }, image.size());
|
||||||
let button = ImageButton::new(image);
|
let button = ImageButton::new(image);
|
||||||
|
|
||||||
@@ -77,17 +80,32 @@ fn render_image(
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType ) {
|
pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType) {
|
||||||
let state = &mut app.image_selected_state;
|
// Set the selected shortcut immediately so UI can show it
|
||||||
//We must have a user to get to this action;
|
app.image_selected_state.selected_shortcut = Some(shortcut.clone());
|
||||||
if let Some(auth_key) = &app.settings.steamgrid_db.auth_key {
|
app.image_selected_state.grid_id = None;
|
||||||
|
|
||||||
|
// We must have a user to get to this action
|
||||||
|
if let Some(auth_key) = app.settings.steamgrid_db.auth_key.clone() {
|
||||||
|
// Create channel to communicate results
|
||||||
|
let (tx, rx) = watch::channel(FetchStatus::Fetching);
|
||||||
|
app.image_selected_state.grid_id_search = rx;
|
||||||
|
|
||||||
|
let app_id = shortcut.app_id();
|
||||||
|
let app_name = shortcut.name().to_string();
|
||||||
|
|
||||||
|
// Spawn the search in the background instead of blocking the UI
|
||||||
|
app.rt.spawn(async move {
|
||||||
let client = steamgriddb_api::Client::new(auth_key);
|
let client = steamgriddb_api::Client::new(auth_key);
|
||||||
let search = CachedSearch::new(&client);
|
let search = CachedSearch::new(&client);
|
||||||
state.grid_id = app
|
let result = search.search(app_id, &app_name).await;
|
||||||
.rt
|
|
||||||
.block_on(search.search(shortcut.app_id(), shortcut.name()))
|
let search_result = match result {
|
||||||
.ok()
|
Ok(grid_id) => Ok(grid_id),
|
||||||
.flatten();
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
};
|
||||||
state.selected_shortcut = Some(shortcut);
|
|
||||||
|
let _ = tx.send(FetchStatus::Fetched(search_result));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use super::{
|
|||||||
useraction::UserAction,
|
useraction::UserAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{ path::Path, thread, time::Duration};
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::get_thumbnails_folder,
|
config::get_thumbnails_folder,
|
||||||
@@ -131,9 +131,39 @@ impl MyEguiApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Poll for async grid_id search results from background task
|
||||||
|
fn poll_grid_id_search(&mut self) {
|
||||||
|
if let FetchStatus::Fetched(result) = &*self.image_selected_state.grid_id_search.borrow() {
|
||||||
|
match result {
|
||||||
|
Ok(Some(grid_id)) => {
|
||||||
|
if self.image_selected_state.grid_id.is_none() {
|
||||||
|
self.image_selected_state.grid_id = Some(*grid_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll for async name search results from background task
|
||||||
|
fn poll_name_search(&mut self) {
|
||||||
|
if let FetchStatus::Fetched(results) = &*self.image_selected_state.name_search.borrow() {
|
||||||
|
if self.image_selected_state.possible_names.is_none() {
|
||||||
|
self.image_selected_state.possible_names = Some(results.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_ui_images(&mut self, ui: &mut egui::Ui) {
|
pub fn render_ui_images(&mut self, ui: &mut egui::Ui) {
|
||||||
self.ensure_steam_users_loaded();
|
self.ensure_steam_users_loaded();
|
||||||
|
|
||||||
|
// Poll for async grid_id search results
|
||||||
|
self.poll_grid_id_search();
|
||||||
|
|
||||||
|
// Poll for async name search results
|
||||||
|
self.poll_name_search();
|
||||||
|
|
||||||
if let Some(error_message) = &self.image_selected_state.settings_error {
|
if let Some(error_message) = &self.image_selected_state.settings_error {
|
||||||
ui.label(error_message);
|
ui.label(error_message);
|
||||||
return;
|
return;
|
||||||
@@ -147,6 +177,7 @@ impl MyEguiApp {
|
|||||||
ui.reset_style();
|
ui.reset_style();
|
||||||
action = self.render_ui_image_action(ui);
|
action = self.render_ui_image_action(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
UserAction::UserSelected(user) => {
|
UserAction::UserSelected(user) => {
|
||||||
self.handle_user_selected(user);
|
self.handle_user_selected(user);
|
||||||
@@ -159,7 +190,8 @@ impl MyEguiApp {
|
|||||||
}
|
}
|
||||||
UserAction::ImageSelected(image) => {
|
UserAction::ImageSelected(image) => {
|
||||||
handle_image_selected(self, image);
|
handle_image_selected(self, image);
|
||||||
thread::sleep(Duration::from_millis(100));
|
// Request repaint to refresh images after download starts
|
||||||
|
ui.ctx().request_repaint();
|
||||||
ui.ctx().forget_all_images();
|
ui.ctx().forget_all_images();
|
||||||
}
|
}
|
||||||
UserAction::BackButton => {
|
UserAction::BackButton => {
|
||||||
|
|||||||
Reference in New Issue
Block a user