feat(app): add egui context for background thread repaint requests
This commit is contained in:
parent
4de452c503
commit
f3e6d3b5b7
4 changed files with 79 additions and 38 deletions
82
src/app.rs
82
src/app.rs
|
|
@ -294,6 +294,10 @@ pub struct App {
|
|||
/// menu or another exit path). The viewport `CloseRequested` handler
|
||||
/// honors this — without it, the close button minimizes to tray.
|
||||
quitting: bool,
|
||||
/// Clone of the egui context, kept so background worker threads (dedup,
|
||||
/// remote fetch) can call `request_repaint()` on completion and wake the
|
||||
/// event loop even when the app is idle or on a different screen.
|
||||
egui_ctx: egui::Context,
|
||||
}
|
||||
|
||||
/// Scratch struct for the per-channel options dialog. Distinct from
|
||||
|
|
@ -691,6 +695,7 @@ impl App {
|
|||
move_to_folder_target: None,
|
||||
tray,
|
||||
quitting: false,
|
||||
egui_ctx: cc.egui_ctx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1098,6 +1103,7 @@ impl App {
|
|||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.dedup_rx = Some(rx);
|
||||
let workers = crate::fingerprint::default_workers();
|
||||
let repaint_ctx = self.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let res = crate::fingerprint::rebuild_and_group(
|
||||
&db, inputs, &valid_paths, workers, &progress.0, &progress.1,
|
||||
|
|
@ -1115,6 +1121,9 @@ impl App {
|
|||
groups
|
||||
});
|
||||
let _ = tx.send(res);
|
||||
// Wake the egui loop so the result is drained in update() even if
|
||||
// the user navigated away from the Maintenance screen.
|
||||
repaint_ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2214,39 +2223,6 @@ impl App {
|
|||
fn maintenance_screen(&mut self, ctx: &egui::Context) {
|
||||
let report = self.health_report.clone().unwrap_or_default();
|
||||
|
||||
// Receive a background remote-library fetch (federation).
|
||||
let remote_result = self.remote_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
||||
if let Some(res) = remote_result {
|
||||
self.remote_rx = None;
|
||||
match res {
|
||||
Ok(lib) => {
|
||||
let n: usize = lib.channels.iter().map(|c| c.videos.len()).sum();
|
||||
self.remote_status =
|
||||
format!("{} channels · {} videos", lib.channels.len(), n);
|
||||
self.remote_library = Some(lib);
|
||||
}
|
||||
Err(e) => {
|
||||
self.remote_status = format!("Error: {e}");
|
||||
self.remote_library = None;
|
||||
}
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// Drain the background dedup result if it's ready.
|
||||
let mut dedup_done = None;
|
||||
if let Some(rx) = &self.dedup_rx {
|
||||
if let Ok(res) = rx.try_recv() { dedup_done = Some(res); }
|
||||
}
|
||||
if let Some(res) = dedup_done {
|
||||
match res {
|
||||
Ok(groups) => self.dedup_groups = groups,
|
||||
Err(e) => self.dedup_error = Some(e),
|
||||
}
|
||||
self.dedup_running = false;
|
||||
self.dedup_rx = None;
|
||||
}
|
||||
|
||||
// Actions are collected during rendering and applied after the closure
|
||||
// to avoid borrowing `self` while the report is borrowed immutably.
|
||||
let mut to_remove: Vec<PathBuf> = Vec::new();
|
||||
|
|
@ -2515,8 +2491,11 @@ impl App {
|
|||
self.remote_status = format!("Connecting to {}…", client.name);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.remote_rx = Some(rx);
|
||||
let repaint_ctx = self.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(client.library());
|
||||
// Wake the egui loop so update() drains the result promptly.
|
||||
repaint_ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -4713,6 +4692,43 @@ impl eframe::App for App {
|
|||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// ── Background remote-library fetch hand-off ────────────────────
|
||||
// Federation peer fetches (start_remote_fetch) deliver over
|
||||
// `remote_rx`. Drain in the main loop, not in a per-screen render:
|
||||
// the result is displayed on the Remotes screen, so draining it only
|
||||
// inside maintenance_screen() left the Remotes screen stuck on
|
||||
// "Connecting…" and busy-looping repaints forever.
|
||||
if let Some(res) = self.remote_rx.as_ref().and_then(|rx| rx.try_recv().ok()) {
|
||||
self.remote_rx = None;
|
||||
match res {
|
||||
Ok(lib) => {
|
||||
let n: usize = lib.channels.iter().map(|c| c.videos.len()).sum();
|
||||
self.remote_status =
|
||||
format!("{} channels · {} videos", lib.channels.len(), n);
|
||||
self.remote_library = Some(lib);
|
||||
}
|
||||
Err(e) => {
|
||||
self.remote_status = format!("Error: {e}");
|
||||
self.remote_library = None;
|
||||
}
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// ── Background dedup result hand-off ────────────────────────────
|
||||
// Drain in the main loop so the scan completes (and the button
|
||||
// re-enables) even if the user navigated away from Maintenance
|
||||
// while the ffmpeg pass was running.
|
||||
if let Some(res) = self.dedup_rx.as_ref().and_then(|rx| rx.try_recv().ok()) {
|
||||
match res {
|
||||
Ok(groups) => self.dedup_groups = groups,
|
||||
Err(e) => self.dedup_error = Some(e),
|
||||
}
|
||||
self.dedup_running = false;
|
||||
self.dedup_rx = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// ── System-tray event drain ─────────────────────────────────────
|
||||
// Tray menu activations arrive on a background-thread channel.
|
||||
// Drain them all each frame and translate into viewport commands.
|
||||
|
|
|
|||
|
|
@ -1329,7 +1329,12 @@ impl Downloader {
|
|||
keep_original: bool,
|
||||
) {
|
||||
let (tx, rx) = channel();
|
||||
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
// ffmpeg writes the encoded stream to `work_out` (a file) and its
|
||||
// diagnostics to stderr (via -loglevel error -stats), so stdout is
|
||||
// never used. Piping it without a reader would let a full 64KB pipe
|
||||
// buffer deadlock ffmpeg against our child.wait() below, so discard
|
||||
// it explicitly.
|
||||
cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::piped());
|
||||
// Spawn before the thread so the watchdog gets a pid (ffmpeg can
|
||||
// also hang on a bad input). On spawn failure, surface it.
|
||||
let mut child = match cmd.spawn() {
|
||||
|
|
|
|||
20
src/web.rs
20
src/web.rs
|
|
@ -378,7 +378,7 @@ struct SettingsPayload {
|
|||
#[serde(skip_deserializing, default)]
|
||||
available_binds: Option<Vec<BindOption>>,
|
||||
/// Selected bind mode (localhost, tailscale, lan, all). Clients can send this on POST to change.
|
||||
#[serde(skip_deserializing, default)]
|
||||
#[serde(default)]
|
||||
bind_mode: Option<String>,
|
||||
/// Whether a password is required for downloads (sent by server only).
|
||||
#[serde(skip_deserializing, default)]
|
||||
|
|
@ -1218,14 +1218,28 @@ async fn auth_middleware(
|
|||
}
|
||||
|
||||
/// True if the query string carries `token=<expected>` (the feed capability
|
||||
/// token). Empty `expected` never matches.
|
||||
/// token). Empty `expected` never matches. The token is compared in constant
|
||||
/// time so response latency doesn't leak how many leading bytes matched.
|
||||
fn query_has_token(query: &str, expected: &str) -> bool {
|
||||
if expected.is_empty() { return false; }
|
||||
query.split('&').any(|kv| {
|
||||
kv.strip_prefix("token=").is_some_and(|v| v == expected)
|
||||
kv.strip_prefix("token=").is_some_and(|v| ct_eq(v.as_bytes(), expected.as_bytes()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Constant-time byte-slice equality. Avoids the early-exit of `==` so a
|
||||
/// network attacker can't binary-search a capability token by timing.
|
||||
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
let mut diff = 0u8;
|
||||
for (x, y) in a.iter().zip(b.iter()) {
|
||||
diff |= x ^ y;
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn get_index() -> impl IntoResponse {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue