diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 50c204e..753f831 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -4,12 +4,17 @@ import ProcessingControls from './lib/components/ProcessingControls.svelte'; import ProgressDisplay from './lib/components/ProgressDisplay.svelte'; import ResultsView from './lib/components/ResultsView.svelte'; + import SettingsPanel from './lib/components/SettingsPanel.svelte'; let connectionOk = $state(false); let selectedPerson = $state(null); let jobStatus = $state('idle'); let progress = $state({ completed: 0, total: 0, message: '' }); + let isJobRunning = $derived( + jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling' + ); + function handleConnectionChange(data) { connectionOk = data.connected; } @@ -31,10 +36,14 @@ {#if connectionOk} +
+ +
+
{#if selectedPerson} diff --git a/frontend/src/lib/components/SettingsPanel.svelte b/frontend/src/lib/components/SettingsPanel.svelte new file mode 100644 index 0000000..8b2f255 --- /dev/null +++ b/frontend/src/lib/components/SettingsPanel.svelte @@ -0,0 +1,382 @@ + + +
+ + + {#if isOpen} +
+ {#if loading} +

Loading settings...

+ {:else if error} +

{error}

+ + {:else} +
+
+ Processing + + + + + + + + + + + + +
+ +
+ Video + + + + + + + + +
+
+ +
+ + {#if saveMessage} + {saveMessage} + {/if} +
+ {/if} +
+ {/if} +
+ + diff --git a/src/web/handlers.rs b/src/web/handlers.rs index 57185b4..cd08e9d 100644 --- a/src/web/handlers.rs +++ b/src/web/handlers.rs @@ -1,5 +1,6 @@ //! HTTP route handlers. +use crate::config::{ProcessingConfig, VideoConfig}; use crate::immich_api::ImmichClient; use crate::job::{run_job, JobParams}; use crate::web::state::{AppState, JobStatus, Progress}; @@ -8,7 +9,7 @@ use axum::{ extract::{Path, State}, http::{header, StatusCode}, response::{Html, IntoResponse, Json, Response}, - routing::{delete, get, post}, + routing::{delete, get, post, put}, Router, }; use serde::{Deserialize, Serialize}; @@ -37,6 +38,8 @@ pub fn create_router(state: AppState) -> Router { .route("/api/cancel", post(cancel_processing)) .route("/api/output", delete(cleanup_all_output)) .route("/api/output/{folder_name}", delete(cleanup_output_folder)) + .route("/api/config", get(get_config)) + .route("/api/config", put(update_config)) // Serve output files (video, images) .nest_service("/output", ServeDir::new(output_dir)) // Serve frontend static files (fallback to index.html for SPA routing) @@ -403,3 +406,109 @@ async fn cleanup_output_folder( message: format!("Deleted folder '{}'", folder_name), })) } + +/// Configuration response (excludes sensitive API credentials). +/// Reuses config types which already derive Serialize. +#[derive(Serialize)] +struct ConfigResponse { + processing: ProcessingConfig, + video: VideoConfig, +} + +/// Get current configuration (excluding sensitive data). +async fn get_config(State(state): State) -> Json { + let config = state.config.read().await; + + Json(ConfigResponse { + processing: config.processing.clone(), + video: config.video.clone(), + }) +} + +/// Configuration update request. +#[derive(Deserialize)] +struct ConfigUpdateRequest { + processing: Option, + video: Option, +} + +#[derive(Deserialize)] +struct ProcessingConfigUpdate { + resize_size: Option, + face_resolution_threshold: Option, + pose_threshold: Option, + ear_threshold: Option, + max_workers: Option, + keep_intermediates: Option, +} + +#[derive(Deserialize)] +struct VideoConfigUpdate { + framerate: Option, + enabled: Option, + codec: Option, + crf: Option, +} + +/// Update configuration. +async fn update_config( + State(state): State, + Json(update): Json, +) -> Result, (StatusCode, String)> { + // Check if a job is running + { + let progress = state.progress.read().await; + if progress.status == JobStatus::Running || progress.status == JobStatus::CompilingVideo { + return Err(( + StatusCode::CONFLICT, + "Cannot update config while a job is running".to_string(), + )); + } + } + + // Update config + { + let mut config = state.config.write().await; + + if let Some(proc) = update.processing { + if let Some(v) = proc.resize_size { + config.processing.resize_size = v; + } + if let Some(v) = proc.face_resolution_threshold { + config.processing.face_resolution_threshold = v; + } + if let Some(v) = proc.pose_threshold { + config.processing.pose_threshold = v; + } + if let Some(v) = proc.ear_threshold { + config.processing.ear_threshold = v; + } + if let Some(v) = proc.max_workers { + config.processing.max_workers = v; + } + if let Some(v) = proc.keep_intermediates { + config.processing.keep_intermediates = v; + } + } + + if let Some(vid) = update.video { + if let Some(v) = vid.framerate { + config.video.framerate = v; + } + if let Some(v) = vid.enabled { + config.video.enabled = v; + } + if let Some(v) = vid.codec { + config.video.codec = v; + } + if let Some(v) = vid.crf { + config.video.crf = v; + } + } + + tracing::info!("Configuration updated"); + } + + // Return updated config + Ok(get_config(State(state)).await) +}