diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index ec784c7..2a63698 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,4 +1,5 @@
@@ -47,7 +112,7 @@ disabled={isJobRunning} /> - {#if selectedPerson} + {#if selectedPerson && !isJobRunning} - import { onMount, onDestroy } from 'svelte'; - let { personId, personName, jobStatus, onupdate } = $props(); let dateFrom = $state(''); let dateTo = $state(''); - let pollInterval = $state(null); let assetCount = $state(null); let loadingCount = $state(false); @@ -51,7 +48,13 @@ const data = await res.json(); if (res.ok && data.success) { - startPolling(); + // Notify parent to start polling + onupdate?.({ + status: 'running', + completed: 0, + total: 0, + message: 'Starting...', + }); } else { onupdate?.({ status: 'error', @@ -77,43 +80,6 @@ console.error('Cancel failed:', e); } } - - async function pollProgress() { - try { - const res = await fetch('/api/progress'); - const data = await res.json(); - - onupdate?.(data); - - if (data.status === 'completed' || data.status === 'cancelled' || data.status === 'error') { - stopPolling(); - } - } catch (e) { - console.error('Poll failed:', e); - } - } - - function startPolling() { - stopPolling(); - pollProgress(); - pollInterval = setInterval(pollProgress, 500); - } - - function stopPolling() { - if (pollInterval) { - clearInterval(pollInterval); - pollInterval = null; - } - } - - onMount(() => { - // Check if there's already a running job - pollProgress(); - }); - - onDestroy(() => { - stopPolling(); - });
@@ -147,15 +113,9 @@
- {#if isRunning} - - {:else} - - {/if} +
@@ -271,13 +231,4 @@ .start-btn:hover { background: #4338ca; } - - .cancel-btn { - background: #dc2626; - color: #fff; - } - - .cancel-btn:hover { - background: #b91c1c; - } diff --git a/frontend/src/lib/components/ProgressDisplay.svelte b/frontend/src/lib/components/ProgressDisplay.svelte index b22b9df..8439fe6 100644 --- a/frontend/src/lib/components/ProgressDisplay.svelte +++ b/frontend/src/lib/components/ProgressDisplay.svelte @@ -25,6 +25,11 @@ error: 'error', }[jobStatus] || ''); + let canCancel = $derived(jobStatus === 'running' || jobStatus === 'compiling_video'); + + // Person being processed + let personDisplay = $derived(progress.person_name || progress.person_id || null); + // Skip statistics from the backend let skipStats = $derived(progress.skip_stats || { face_too_small: 0, @@ -39,8 +44,6 @@ total: 0, }); - let hasSkips = $derived(skipStats.total > 0); - // Calculate kept (successful) count: completed - skipped let keptCount = $derived(Math.max(0, progress.completed - skipStats.total)); @@ -58,14 +61,32 @@ { label: 'Crop failed', count: skipStats.crop_failed }, ].filter(r => r.count > 0) ); + + async function cancelProcessing() { + try { + await fetch('/api/cancel', { method: 'POST' }); + } catch (e) { + console.error('Cancel failed:', e); + } + }
- {statusLabel} - {#if progress.total > 0} - {progress.completed} / {progress.total} - {/if} +
+ {statusLabel} + {#if personDisplay} + {personDisplay} + {/if} +
+
+ {#if progress.total > 0} + {progress.completed} / {progress.total} + {/if} + {#if canCancel} + + {/if} +
{#if jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling'} @@ -114,6 +135,18 @@ margin-bottom: 1rem; } + .header-left { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .person-name { + font-size: 0.875rem; + color: #e0e0e0; + font-weight: 500; + } + .status { font-weight: 600; font-size: 0.875rem; @@ -142,11 +175,33 @@ color: #f87171; } + .header-right { + display: flex; + align-items: center; + gap: 1rem; + } + .count { font-size: 0.875rem; color: #888; } + .cancel-btn { + padding: 0.375rem 0.75rem; + background: #dc2626; + border: none; + border-radius: 4px; + color: #fff; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease; + } + + .cancel-btn:hover { + background: #b91c1c; + } + .progress-bar { height: 8px; background: #333; diff --git a/src/job/mod.rs b/src/job/mod.rs index 5dc2d35..1ab2fd4 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -97,18 +97,20 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat // Clear the cancellation token when done state.clear_cancel_token().await; - // Get final skip stats from progress - let final_skip_stats = state.progress.read().await.skip_stats.clone(); + // Get final state from progress (skip stats and person info) + let final_progress = state.progress.read().await.clone(); match result { Ok(output_path) => { state .update_progress(Progress { status: JobStatus::Completed, - completed: 0, - total: 0, + completed: final_progress.completed, + total: final_progress.total, message: Some(format!("Video saved to: {}", output_path.display())), - skip_stats: final_skip_stats, + skip_stats: final_progress.skip_stats, + person_id: final_progress.person_id, + person_name: final_progress.person_name, }) .await; } @@ -116,10 +118,12 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat state .update_progress(Progress { status: JobStatus::Cancelled, - completed: 0, - total: 0, + completed: final_progress.completed, + total: final_progress.total, message: Some("Job cancelled by user".to_string()), - skip_stats: final_skip_stats, + skip_stats: final_progress.skip_stats, + person_id: final_progress.person_id, + person_name: final_progress.person_name, }) .await; } @@ -128,10 +132,12 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat state .update_progress(Progress { status: JobStatus::Error(e.to_string()), - completed: 0, - total: 0, + completed: final_progress.completed, + total: final_progress.total, message: Some(e.to_string()), - skip_stats: final_skip_stats, + skip_stats: final_progress.skip_stats, + person_id: final_progress.person_id, + person_name: final_progress.person_name, }) .await; } @@ -193,6 +199,8 @@ async fn run_job_inner( total: 0, message: Some("Fetching assets from Immich...".to_string()), skip_stats: SkipStats::default(), + person_id: Some(params.person_id.clone()), + person_name: params.person_name.clone(), }) .await; @@ -244,6 +252,8 @@ async fn run_job_inner( total, message: Some(format!("Processing {} images...", total)), skip_stats: SkipStats::default(), + person_id: Some(params.person_id.clone()), + person_name: params.person_name.clone(), }) .await; @@ -267,6 +277,10 @@ async fn run_job_inner( let mut handles = Vec::with_capacity(assets_with_faces.len()); + // Clone person info for use in spawned tasks + let task_person_id = params.person_id.clone(); + let task_person_name = params.person_name.clone(); + for (asset, face_data) in assets_with_faces { // Check for cancellation before spawning more tasks if cancel_token.is_cancelled() { @@ -295,6 +309,10 @@ async fn run_job_inner( let skip_decode_failed = skip_decode_failed.clone(); let skip_crop_failed = skip_crop_failed.clone(); + // Clone person info for this task + let person_id = task_person_id.clone(); + let person_name = task_person_name.clone(); + let handle = tokio::spawn(async move { // Check cancellation at start of task if task_cancel_token.is_cancelled() { @@ -353,6 +371,8 @@ async fn run_job_inner( total, message: Some(format!("Processing images... ({}/{})", done, total)), skip_stats: current_skip_stats, + person_id: Some(person_id.clone()), + person_name: person_name.clone(), }) .await; } @@ -419,6 +439,8 @@ async fn run_job_inner( total, message: Some("Processing complete, preparing video...".to_string()), skip_stats: skip_stats.clone(), + person_id: Some(params.person_id.clone()), + person_name: params.person_name.clone(), }) .await; @@ -442,6 +464,8 @@ async fn run_job_inner( total: successful as u32, message: Some("Compiling video...".to_string()), skip_stats: skip_stats.clone(), + person_id: Some(params.person_id.clone()), + person_name: params.person_name.clone(), }) .await; diff --git a/src/web/handlers.rs b/src/web/handlers.rs index 022eb1c..b84de5b 100644 --- a/src/web/handlers.rs +++ b/src/web/handlers.rs @@ -258,6 +258,8 @@ struct ProgressResponse { total: u32, message: Option, skip_stats: SkipStatsResponse, + person_id: Option, + person_name: Option, } async fn get_progress(State(state): State) -> Json { @@ -292,6 +294,8 @@ async fn get_progress(State(state): State) -> Json { crop_failed: skip_stats.crop_failed, total: skip_stats.total(), }, + person_id: progress.person_id.clone(), + person_name: progress.person_name.clone(), }) } @@ -322,7 +326,7 @@ async fn start_processing( } } - // Reset progress + // Reset progress with person info state .update_progress(Progress { status: JobStatus::Running, @@ -330,6 +334,8 @@ async fn start_processing( total: 0, message: Some("Starting...".to_string()), skip_stats: SkipStats::default(), + person_id: Some(request.person_id.clone()), + person_name: request.person_name.clone(), }) .await; diff --git a/src/web/state.rs b/src/web/state.rs index f672419..9cbe299 100644 --- a/src/web/state.rs +++ b/src/web/state.rs @@ -54,6 +54,9 @@ pub struct Progress { pub total: u32, pub message: Option, pub skip_stats: SkipStats, + /// The person being processed (for display when resuming) + pub person_id: Option, + pub person_name: Option, } impl Default for Progress { @@ -64,6 +67,8 @@ impl Default for Progress { total: 0, message: None, skip_stats: SkipStats::default(), + person_id: None, + person_name: None, } } }