use dioxus::prelude::*; use crate::client::ConfigResponse; #[component] pub fn Settings( config: ConfigResponse, on_add_root: EventHandler, on_remove_root: EventHandler, on_toggle_watch: EventHandler, on_update_poll_interval: EventHandler, on_update_ignore_patterns: EventHandler>, #[props(default)] on_update_ui_config: Option< EventHandler, >, ) -> Element { let mut new_root = use_signal(String::new); let mut editing_poll = use_signal(|| false); let mut poll_input = use_signal(String::new); let mut poll_error = use_signal(|| Option::::None); let mut editing_patterns = use_signal(|| false); let mut patterns_input = use_signal(String::new); let writable = config.config_writable; let watch_enabled = config.scanning.watch; let host_port = format!("{}:{}", config.server.host, config.server.port); let db_path = config.database_path.clone().unwrap_or_default(); let root_count = config.roots.len(); rsx! { div { class: "settings-layout", // Configuration source div { class: "settings-card", div { class: "settings-card-header", h3 { class: "settings-card-title", "Configuration Source" } if writable { span { class: "badge badge-success", "Writable" } } else { span { class: "badge badge-warning", "Read-only" } } } div { class: "settings-card-body", if let Some(ref path) = config.config_path { div { class: "info-row", label { class: "form-label", "Config Path" } span { class: "info-value mono", "{path}" } } } if !writable { div { class: "settings-notice settings-notice-warning", "Configuration is read-only. Changes cannot be persisted to disk. " "To enable editing, ensure the config file exists and is writable by the server process." } } } } // Server health div { class: "settings-card", div { class: "settings-card-header", h3 { class: "settings-card-title", "Server Info" } } div { class: "settings-card-body", div { class: "info-row", div { class: "form-label-row", label { class: "form-label", "Backend" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "The storage backend used by the server (SQLite or PostgreSQL)." } } } span { class: "info-value badge badge-neutral", "{config.backend}" } } div { class: "info-row", div { class: "form-label-row", label { class: "form-label", "Server Address" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "The address and port the server is listening on." } } } span { class: "info-value mono", "{host_port}" } } div { class: "info-row", div { class: "form-label-row", label { class: "form-label", "Database Path" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "File path to the SQLite database, or connection info for PostgreSQL." } } } span { class: "info-value mono", "{db_path}" } } } } // Root directories div { class: "settings-card", div { class: "settings-card-header", div { class: "form-label-row", h3 { class: "settings-card-title", "Root Directories" } span { class: "badge badge-neutral", "{root_count}" } } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Directories that Pinakes scans for media files. Only existing directories can be added." } } } div { class: "settings-card-body", if config.roots.is_empty() { p { class: "text-muted", "No root directories configured." } } else { div { class: "root-list", for root in config.roots.iter() { div { class: "root-item", key: "{root}", span { class: "mono root-path", "{root}" } button { class: "btn btn-danger btn-sm", disabled: !writable, onclick: { let root = root.clone(); move |_| { if writable { on_remove_root.call(root.clone()); } } }, "Remove" } } } } } div { class: "form-row", input { r#type: "text", placeholder: "/path/to/root...", value: "{new_root}", disabled: !writable, oninput: move |e| new_root.set(e.value()), onkeypress: move |e: KeyboardEvent| { if writable && e.key() == Key::Enter { let path = new_root.read().clone(); if !path.is_empty() { on_add_root.call(path); new_root.set(String::new()); } } }, } button { class: "btn btn-secondary", disabled: !writable, onclick: move |_| { if writable { let mut new_root = new_root; spawn(async move { if let Some(handle) = rfd::AsyncFileDialog::new().pick_folder().await { new_root.set(handle.path().to_string_lossy().to_string()); } }); } }, "Browse..." } button { class: "btn btn-primary", disabled: !writable, onclick: move |_| { if writable { let path = new_root.read().clone(); if !path.is_empty() { on_add_root.call(path); new_root.set(String::new()); } } }, "Add Root" } } } } // Scanning div { class: "settings-card", div { class: "settings-card-header", h3 { class: "settings-card-title", "Scanning" } } div { class: "settings-card-body", // File watching toggle div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "File Watching" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "When enabled, Pinakes monitors root directories for new, modified, or deleted files in real time using filesystem events." } } } div { class: if writable { "toggle" } else { "toggle toggle-disabled" }, onclick: move |_| { if writable { on_toggle_watch.call(!watch_enabled); } }, div { class: if watch_enabled { "toggle-track active" } else { "toggle-track" }, div { class: "toggle-thumb" } } } } // Poll interval div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Poll Interval" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "How often (in seconds) Pinakes polls for file changes when watch mode is not available or as a fallback." } } } if *editing_poll.read() { div { class: "settings-inline-edit", input { r#type: "number", min: "1", value: "{poll_input}", class: "input-sm", oninput: move |e| { poll_input.set(e.value()); // Clear error on new input poll_error.set(None); }, onkeypress: move |e: KeyboardEvent| { if e.key() == Key::Enter { let raw = poll_input.read().clone(); match raw.parse::() { Ok(secs) if secs > 0 => { on_update_poll_interval.call(secs); editing_poll.set(false); poll_error.set(None); } _ => { poll_error .set(Some("Enter a positive integer (seconds).".to_string())); } } } }, } span { class: "input-suffix", "seconds" } button { class: "btn btn-primary btn-sm", onclick: move |_| { let raw = poll_input.read().clone(); match raw.parse::() { Ok(secs) if secs > 0 => { on_update_poll_interval.call(secs); editing_poll.set(false); poll_error.set(None); } _ => { poll_error.set(Some("Enter a positive integer (seconds).".to_string())); } } }, "Save" } button { class: "btn btn-ghost btn-sm", onclick: move |_| { editing_poll.set(false); poll_error.set(None); }, "Cancel" } } if let Some(ref err) = *poll_error.read() { p { class: "field-error", "{err}" } } } else { div { class: "flex-row", span { class: "info-value", "{config.scanning.poll_interval_secs}s" } button { class: "btn btn-ghost btn-sm", disabled: !writable, onclick: { let current = config.scanning.poll_interval_secs; move |_| { if writable { poll_input.set(current.to_string()); poll_error.set(None); editing_poll.set(true); } } }, "Edit" } } } } // Ignore patterns div { class: "settings-field", div { class: "settings-field-header", div { class: "form-label-row", label { class: "form-label", "Ignore Patterns" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Glob patterns for files and directories to skip during scanning. One pattern per line." } } } if *editing_patterns.read() { div { class: "flex-row", button { class: "btn btn-primary btn-sm", onclick: move |_| { let input = patterns_input.read().clone(); let patterns: Vec = input .lines() .map(|l| l.trim().to_string()) .filter(|l| !l.is_empty()) .collect(); on_update_ignore_patterns.call(patterns); editing_patterns.set(false); }, "Save" } button { class: "btn btn-ghost btn-sm", onclick: move |_| editing_patterns.set(false), "Cancel" } } } else { button { class: "btn btn-ghost btn-sm", disabled: !writable, onclick: { let patterns = config.scanning.ignore_patterns.clone(); move |_| { if writable { patterns_input.set(patterns.join("\n")); editing_patterns.set(true); } } }, "Edit" } } } if *editing_patterns.read() { div { class: "settings-patterns-edit", textarea { value: "{patterns_input}", oninput: move |e| patterns_input.set(e.value()), rows: "8", class: "patterns-textarea", placeholder: "One pattern per line, e.g.:\n*.tmp\n.git/**\nnode_modules/**", } p { class: "text-muted text-sm", "Enter one glob pattern per line. Empty lines are ignored." } } } else { if config.scanning.ignore_patterns.is_empty() { p { class: "text-muted text-sm", "No ignore patterns configured." } } else { div { class: "patterns-list", for pattern in config.scanning.ignore_patterns.iter() { span { class: "pattern-chip mono", "{pattern}" } } } } } } } } // UI preferences div { class: "settings-card", div { class: "settings-card-header", h3 { class: "settings-card-title", "UI Preferences" } } div { class: "settings-card-body", // Theme div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Theme" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Choose between dark and light themes." } } } select { value: "{config.ui.theme}", onchange: { let handler = on_update_ui_config; move |e: Event| { if let Some(ref h) = handler { h.call(serde_json::json!({ "theme" : e.value() })); } } }, option { value: "dark", "Dark" } option { value: "light", "Light" } option { value: "system", "System" } } } // Default view div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Default View" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "The view shown when the application starts." } } } select { value: "{config.ui.default_view}", onchange: { let handler = on_update_ui_config; move |e: Event| { if let Some(ref h) = handler { h.call(serde_json::json!({ "default_view" : e.value() })); } } }, option { value: "library", "Library" } option { value: "search", "Search" } } } // Default page size div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Default Page Size" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Number of items shown per page by default." } } } select { value: "{config.ui.default_page_size}", onchange: { let handler = on_update_ui_config; move |e: Event| { if let Some(ref h) = handler && let Ok(size) = e.value().parse::() { h.call(serde_json::json!({ "default_page_size" : size })); } } }, option { value: "24", "24" } option { value: "48", "48" } option { value: "96", "96" } option { value: "200", "200" } } } // Default view mode div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Default View Mode" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Whether to show items in a grid or table layout." } } } select { value: "{config.ui.default_view_mode}", onchange: { let handler = on_update_ui_config; move |e: Event| { if let Some(ref h) = handler { h.call(serde_json::json!({ "default_view_mode" : e.value() })); } } }, option { value: "grid", "Grid" } option { value: "table", "Table" } } } // Auto-play media div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Auto-play Media" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Automatically start playback when opening audio or video." } } } { let autoplay = config.ui.auto_play_media; let handler = on_update_ui_config; rsx! { div { class: "toggle", onclick: move |_| { if let Some(ref h) = handler { h.call(serde_json::json!({ "auto_play_media" : ! autoplay })); } }, div { class: if autoplay { "toggle-track active" } else { "toggle-track" }, div { class: "toggle-thumb" } } } } } } // Show thumbnails div { class: "settings-field", div { class: "form-label-row", label { class: "form-label", "Show Thumbnails" } span { class: "tooltip-trigger", "?" span { class: "tooltip-text", "Display thumbnail previews in library and search views." } } } { let show_thumbs = config.ui.show_thumbnails; let handler = on_update_ui_config; rsx! { div { class: "toggle", onclick: move |_| { if let Some(ref h) = handler { h.call(serde_json::json!({ "show_thumbnails" : ! show_thumbs })); } }, div { class: if show_thumbs { "toggle-track active" } else { "toggle-track" }, div { class: "toggle-thumb" } } } } } } } } } } }