feat: added rerender when new screenshot is taken (#745)

This commit is contained in:
Maciek 2026-08-26 12:37:57 +02:00 • committed by GitHub
parent f2ecde8927
commit 795d80870b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 176 additions and 7 deletions

77
Cargo.lock generated
View file

@ -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",

View file

@ -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"}

View file

@ -38,6 +38,8 @@ uuid.workspace = true
bytes.workspace = true
sysinfo.workspace = true
notify.workspace = true
anyhow.workspace = true
regex.workspace = true

View file

@ -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,
};

View file

@ -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,

View file

@ -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<Arc<Semaphore>> = OnceLock::new();
@ -84,6 +88,90 @@ pub fn use_local_image(path: PathBuf, max_edge: u32) -> UseQuery<LocalImageQuery
))
}
const WATCH_QUIET: Duration = Duration::from_millis(400);
pub fn use_screenshot_folder_watch(
folder: Option<PathBuf>,
query: UseQuery<ClusterScreenshotsQuery>,
) {
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<ClusterScreenshotsQuery>,
) -> 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<notify::Event>| {
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: &notify::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<ClusterScreenshotsQuery>,
) -> Option<Vec<ScreenshotInfo>> {

View file

@ -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();