From 795d80870bbdc6359a421c141e03f10473467727 Mon Sep 17 00:00:00 2001 From: Maciek <108873945+rozwader@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:37:57 +0200 Subject: [PATCH] feat: added rerender when new screenshot is taken (#745) --- Cargo.lock | 77 ++++++++++++++++ Cargo.toml | 1 + packages/oneclient_app/Cargo.toml | 2 + packages/oneclient_app/src/hooks/mod.rs | 3 +- .../oneclient_app/src/hooks/queries/mod.rs | 4 +- .../src/hooks/queries/screenshots.rs | 92 ++++++++++++++++++- .../src/view/app/cluster/screenshots.rs | 4 +- 7 files changed, 176 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d843a0f..b1ef387d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2416,6 +2416,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.1.31" @@ -3221,6 +3230,26 @@ dependencies = [ "web-time", ] +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags 2.13.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + [[package]] name = "interfrost" version = "1.3.2" @@ -3407,6 +3436,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3844,6 +3893,33 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -4420,6 +4496,7 @@ dependencies = [ "freya-skia-safe", "image", "mimalloc", + "notify", "oneclient_auth", "oneclient_cluster", "oneclient_common", diff --git a/Cargo.toml b/Cargo.toml index 22395c66..6b7952aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,7 @@ dunce = {version = "=1.0.5"} htmd = {version = "=0.5.4"} indicatif = {version = "=0.18.4", features = ["tokio"]} junction = {version = "=2.0.0"} +notify = {version = "=8.2.0"} parking_lot = {version = "=0.12.5"} rand = {version = "=0.10.1"} regex = {version = "=1.12.3"} diff --git a/packages/oneclient_app/Cargo.toml b/packages/oneclient_app/Cargo.toml index 107dc427..1c5c9b86 100644 --- a/packages/oneclient_app/Cargo.toml +++ b/packages/oneclient_app/Cargo.toml @@ -38,6 +38,8 @@ uuid.workspace = true bytes.workspace = true sysinfo.workspace = true +notify.workspace = true + anyhow.workspace = true regex.workspace = true diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index ec3da18a..a106dd94 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -51,7 +51,8 @@ pub use queries::{ use_package_project, use_package_search, use_package_versions, use_package_versions_when, use_player_profile, use_player_skin, use_provider_versions, use_refresh_account, use_refresh_all_accounts, - use_remove_account, use_screenshot_action, use_set_default_account, use_terms, use_upload_log, + use_remove_account, use_screenshot_action, use_screenshot_folder_watch, use_set_default_account, + use_terms, use_upload_log, use_version_metadata, use_versions, version_list, versions_metadata, versions_total, }; diff --git a/packages/oneclient_app/src/hooks/queries/mod.rs b/packages/oneclient_app/src/hooks/queries/mod.rs index 1fdc560e..9bf7949e 100644 --- a/packages/oneclient_app/src/hooks/queries/mod.rs +++ b/packages/oneclient_app/src/hooks/queries/mod.rs @@ -1,7 +1,5 @@ use crate::hooks::use_settings_snapshot; -/// Not the url itself just enough to notice when the meta host setting -/// changes so a different host refetches instead of serving the old answer fn use_meta_url_key() -> String { use_settings_snapshot() .settings @@ -88,7 +86,7 @@ pub use packages::{ pub use player_profile::use_player_profile; pub use screenshots::{ ScreenshotAction, UseScreenshotAction, invalidate_screenshots_queries, try_cluster_screenshots, - use_cluster_screenshots, use_local_image, use_screenshot_action, + use_cluster_screenshots, use_local_image, use_screenshot_action, use_screenshot_folder_watch, }; pub use settings_profiles::{ try_game_profile, use_cluster_profile, use_cluster_settings, use_game_profile, diff --git a/packages/oneclient_app/src/hooks/queries/screenshots.rs b/packages/oneclient_app/src/hooks/queries/screenshots.rs index 89d63ed2..c705959d 100644 --- a/packages/oneclient_app/src/hooks/queries/screenshots.rs +++ b/packages/oneclient_app/src/hooks/queries/screenshots.rs @@ -1,13 +1,17 @@ -use std::path::PathBuf; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; +use std::time::Duration; use bytes::Bytes; +use freya::prelude::{spawn, use_hook}; use freya::query::{ Mutation, MutationCapability, QueriesStorage, Query, QueryCapability, UseMutation, UseQuery, use_mutation, use_query, }; +use notify::{EventKind, RecursiveMode, Watcher}; use oneclient_core::{LauncherError, ScreenshotInfo}; -use tokio::sync::Semaphore; +use tokio::sync::{Semaphore, mpsc}; static LOCAL_IMAGE_SEMAPHORE: OnceLock> = OnceLock::new(); @@ -84,6 +88,90 @@ pub fn use_local_image(path: PathBuf, max_edge: u32) -> UseQuery, + query: UseQuery, +) { + use_hook(move || { + let Some(folder) = folder else { + return; + }; + + spawn(async move { + if let Err(err) = watch_folder(&folder, query).await { + tracing::warn!( + folder = %folder.display(), + error = %err, + "not watching the screenshot folder; the list will refresh on re-entry only" + ); + } + }); + }); +} + +async fn watch_folder( + folder: &Path, + query: UseQuery, +) -> notify::Result<()> { + // If there is no screenshots folder it creates it + std::fs::create_dir_all(folder)?; + + let (tx, mut rx) = mpsc::unbounded_channel(); + let mut watcher = notify::recommended_watcher(move |event: notify::Result| { + match event { + Ok(event) if touches_image(&event) => { + let _ = tx.send(()); + } + Ok(_) => {} + Err(err) => tracing::debug!(error = %err, "screenshot watcher reported an error"), + } + })?; + watcher.watch(folder, RecursiveMode::NonRecursive)?; + + let mut pending = false; + loop { + tokio::select! { + biased; + + event = rx.recv() => { + if event.is_none() { + return Ok(()); + } + pending = true; + } + + () = quiet_period(pending) => { + pending = false; + query.invalidate(); + } + } + } +} + +async fn quiet_period(pending: bool) { + if pending { + tokio::time::sleep(WATCH_QUIET).await; + } else { + std::future::pending::<()>().await; + } +} + +fn touches_image(event: ¬ify::Event) -> bool { + if matches!(event.kind, EventKind::Access(_)) { + return false; + } + + event.paths.iter().any(|path| { + path.extension().and_then(OsStr::to_str).is_some_and(|ext| { + ["png", "jpg", "jpeg"] + .iter() + .any(|known| ext.eq_ignore_ascii_case(known)) + }) + }) +} + pub fn try_cluster_screenshots( query: &UseQuery, ) -> Option> { diff --git a/packages/oneclient_app/src/view/app/cluster/screenshots.rs b/packages/oneclient_app/src/view/app/cluster/screenshots.rs index 77c6b538..7a6670fc 100644 --- a/packages/oneclient_app/src/view/app/cluster/screenshots.rs +++ b/packages/oneclient_app/src/view/app/cluster/screenshots.rs @@ -9,7 +9,7 @@ use crate::components::{ Button, ContextMenu, Icon, IconType, LocalImage, OverlayPopup, ScreenshotViewer, ScrollArea, Segment, SegmentedControl, open_folder_button, }; -use crate::hooks::{ScreenshotAction, query_is_loading, try_cluster_screenshots, use_cluster_screenshots, use_dispatch, use_screenshot_action, use_view_state}; +use crate::hooks::{ScreenshotAction, query_is_loading, try_cluster_screenshots, use_cluster_screenshots, use_dispatch, use_screenshot_action, use_screenshot_folder_watch, use_view_state}; use crate::layout::cluster_content; use crate::theme::colors; use crate::ui::{border_all_color, flow_grid, fmt_date, grid_columns_for_width}; @@ -39,6 +39,8 @@ impl Component for ClusterScreenshots { let folder = cluster.game_dir().ok().map(|d| d.join("screenshots")); let query = use_cluster_screenshots(self.cluster_id); + // Otherwise a screenshot taken in game only shows up on the next visit + use_screenshot_folder_watch(folder.clone(), query); let action = use_screenshot_action(); let shots = try_cluster_screenshots(&query).unwrap_or_default();