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};
|
||||
|
||||
|
||||
/// Result type for grid ID search operations
|
||||
pub type GridIdSearchResult = Result<Option<usize>, String>;
|
||||
|
||||
pub struct ImageSelectState {
|
||||
pub selected_shortcut: Option<GameType>,
|
||||
pub grid_id: Option<usize>,
|
||||
@@ -21,6 +24,12 @@ pub struct ImageSelectState {
|
||||
pub steam_games: Option<Vec<crate::steam::SteamGameInfo>>,
|
||||
|
||||
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,
|
||||
image_options: watch::channel(FetchStatus::NeedsFetched).1,
|
||||
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::{
|
||||
steamgriddb::CachedSearch,
|
||||
ui::{
|
||||
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
|
||||
.image_selected_state
|
||||
.selected_shortcut
|
||||
.as_ref()
|
||||
.map(|s| s.name())
|
||||
.unwrap_or_default();
|
||||
let auth_key = app
|
||||
.settings
|
||||
.steamgrid_db
|
||||
.auth_key
|
||||
.clone()
|
||||
.map(|s| s.name().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
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.name_search = rx;
|
||||
|
||||
// Clear any existing possible_names so we show loading state
|
||||
app.image_selected_state.possible_names = None;
|
||||
|
||||
// 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 = app.rt.block_on(client.search(app_name));
|
||||
app.image_selected_state.possible_names = search_results.ok();
|
||||
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 {
|
||||
let (_path, key) = shortcut.key(image_type, Path::new(&user_path));
|
||||
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 button = ImageButton::new(image);
|
||||
ui.add_sized(calced,button).on_hover_text(text).clicked()
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use egui::ImageButton;
|
||||
use steam_shortcuts_util::shortcut::ShortcutOwned;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::{
|
||||
steam::SteamUsersInfo,
|
||||
@@ -11,7 +12,7 @@ use crate::{
|
||||
gametype::GameType, hasimagekey::HasImageKey,
|
||||
useraction::UserAction,
|
||||
},
|
||||
MyEguiApp,
|
||||
FetchStatus, MyEguiApp,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,7 +67,9 @@ fn render_image(
|
||||
&ImageType::Grid,
|
||||
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 button = ImageButton::new(image);
|
||||
|
||||
@@ -77,17 +80,32 @@ fn render_image(
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType ) {
|
||||
let state = &mut app.image_selected_state;
|
||||
//We must have a user to get to this action;
|
||||
if let Some(auth_key) = &app.settings.steamgrid_db.auth_key {
|
||||
pub fn handle_shortcut_selected(app: &mut MyEguiApp, shortcut: GameType) {
|
||||
// Set the selected shortcut immediately so UI can show it
|
||||
app.image_selected_state.selected_shortcut = Some(shortcut.clone());
|
||||
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 search = CachedSearch::new(&client);
|
||||
state.grid_id = app
|
||||
.rt
|
||||
.block_on(search.search(shortcut.app_id(), shortcut.name()))
|
||||
.ok()
|
||||
.flatten();
|
||||
let result = search.search(app_id, &app_name).await;
|
||||
|
||||
let search_result = match result {
|
||||
Ok(grid_id) => Ok(grid_id),
|
||||
Err(e) => Err(e.to_string()),
|
||||
};
|
||||
|
||||
let _ = tx.send(FetchStatus::Fetched(search_result));
|
||||
});
|
||||
}
|
||||
state.selected_shortcut = Some(shortcut);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::{
|
||||
useraction::UserAction,
|
||||
};
|
||||
|
||||
use std::{ path::Path, thread, time::Duration};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
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) {
|
||||
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 {
|
||||
ui.label(error_message);
|
||||
return;
|
||||
@@ -147,6 +177,7 @@ impl MyEguiApp {
|
||||
ui.reset_style();
|
||||
action = self.render_ui_image_action(ui);
|
||||
});
|
||||
|
||||
match action {
|
||||
UserAction::UserSelected(user) => {
|
||||
self.handle_user_selected(user);
|
||||
@@ -159,7 +190,8 @@ impl MyEguiApp {
|
||||
}
|
||||
UserAction::ImageSelected(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();
|
||||
}
|
||||
UserAction::BackButton => {
|
||||
|
||||
Reference in New Issue
Block a user