From bea42524f2296100aae2b60c517969759f279502 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:26:59 +0300 Subject: [PATCH 1/8] pinakes-ui: supply `local_state` to `Conditional` and `Progress`; remove `last_refresh` Signed-off-by: NotAShelf Change-Id: Ib513b5846d6c74bfe821da195b7080af6a6a6964 --- crates/pinakes-ui/src/plugin_ui/registry.rs | 18 ++---- crates/pinakes-ui/src/plugin_ui/renderer.rs | 64 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/crates/pinakes-ui/src/plugin_ui/registry.rs b/crates/pinakes-ui/src/plugin_ui/registry.rs index 4ad2b5c..8fde3d0 100644 --- a/crates/pinakes-ui/src/plugin_ui/registry.rs +++ b/crates/pinakes-ui/src/plugin_ui/registry.rs @@ -41,15 +41,13 @@ pub struct PluginPage { #[derive(Debug, Clone)] pub struct PluginRegistry { /// API client for fetching pages from server - client: ApiClient, + client: ApiClient, /// Cached pages: (`plugin_id`, `page_id`) -> `PluginPage` - pages: HashMap<(String, String), PluginPage>, + pages: HashMap<(String, String), PluginPage>, /// Cached widgets: (`plugin_id`, `widget_id`) -> `UiWidget` - widgets: Vec<(String, UiWidget)>, + widgets: Vec<(String, UiWidget)>, /// Merged CSS custom property overrides from all enabled plugins - theme_vars: HashMap, - /// Last refresh timestamp - last_refresh: Option>, + theme_vars: HashMap, } impl PluginRegistry { @@ -60,7 +58,6 @@ impl PluginRegistry { pages: HashMap::new(), widgets: Vec::new(), theme_vars: HashMap::new(), - last_refresh: None, } } @@ -206,14 +203,8 @@ impl PluginRegistry { self.pages = tmp.pages; self.widgets = tmp.widgets; self.theme_vars = tmp.theme_vars; - self.last_refresh = Some(chrono::Utc::now()); Ok(()) } - - /// Get last refresh time - pub const fn last_refresh(&self) -> Option> { - self.last_refresh - } } impl Default for PluginRegistry { @@ -346,7 +337,6 @@ mod tests { let registry = PluginRegistry::default(); assert!(registry.is_empty()); assert_eq!(registry.all_pages().len(), 0); - assert!(registry.last_refresh().is_none()); } #[test] diff --git a/crates/pinakes-ui/src/plugin_ui/renderer.rs b/crates/pinakes-ui/src/plugin_ui/renderer.rs index 0272e6b..63a9e7f 100644 --- a/crates/pinakes-ui/src/plugin_ui/renderer.rs +++ b/crates/pinakes-ui/src/plugin_ui/renderer.rs @@ -708,7 +708,8 @@ pub fn render_element( } else if let Some(arr) = items.and_then(|v| v.as_array()) { for item in arr { { - let url_opt = media_grid_image_url(item); + let base = ctx.client.peek().base_url().to_string(); + let url_opt = media_grid_image_url(item, &base); let label = media_grid_label(item); rsx! { div { class: "media-grid-item", @@ -795,7 +796,16 @@ pub fn render_element( .map(|obj| { obj .iter() - .map(|(k, v)| (k.clone(), value_to_display_string(v))) + .filter_map(|(k, v)| { + match v { + // Skip nested objects and arrays; they are not meaningful as + // single-line description terms. + serde_json::Value::Object(_) | serde_json::Value::Array(_) => { + None + }, + _ => Some((format_key_name(k), value_to_display_string(v))), + } + }) .collect() }) .unwrap_or_default(); @@ -1044,7 +1054,7 @@ pub fn render_element( max, show_percentage, } => { - let eval_ctx = data.as_json(); + let eval_ctx = build_ctx(data, &ctx.local_state.read()); let pct = evaluate_expression_as_f64(value, &eval_ctx); let fraction = if *max > 0.0 { (pct / max).clamp(0.0, 1.0) @@ -1116,7 +1126,7 @@ pub fn render_element( then, else_element, } => { - let eval_ctx = data.as_json(); + let eval_ctx = build_ctx(data, &ctx.local_state.read()); if evaluate_expression_as_bool(condition, &eval_ctx) { render_element(then, data, actions, ctx) } else if let Some(else_el) = else_element { @@ -1244,7 +1254,10 @@ fn render_chart_data( // MediaGrid helpers /// Probe a JSON object for common image URL fields. -fn media_grid_image_url(item: &serde_json::Value) -> Option { +fn media_grid_image_url( + item: &serde_json::Value, + base_url: &str, +) -> Option { for key in &[ "thumbnail_url", "thumbnail", @@ -1260,12 +1273,22 @@ fn media_grid_image_url(item: &serde_json::Value) -> Option { } } } + // Pinakes media items: construct absolute thumbnail URL from id when + // has_thumbnail is true. Relative paths don't work for in the + // desktop WebView context. + if item.get("has_thumbnail").and_then(|v| v.as_bool()) == Some(true) { + if let Some(id) = item.get("id").and_then(|v| v.as_str()) { + if !id.is_empty() { + return Some(format!("{base_url}/api/v1/media/{id}/thumbnail")); + } + } + } None } /// Probe a JSON object for a human-readable label. fn media_grid_label(item: &serde_json::Value) -> String { - for key in &["title", "name", "label", "caption"] { + for key in &["title", "name", "label", "caption", "file_name"] { if let Some(s) = item.get(*key).and_then(|v| v.as_str()) { if !s.is_empty() { return s.to_string(); @@ -1601,12 +1624,41 @@ fn safe_col_width_css(w: &str) -> Option { None } +/// Convert a snake_case JSON key to a human-readable title. +/// `avg_file_size_bytes` → `Avg File Size Bytes` +fn format_key_name(key: &str) -> String { + key + .split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => { + first.to_uppercase().collect::() + chars.as_str() + }, + } + }) + .collect::>() + .join(" ") +} + #[cfg(test)] mod tests { use pinakes_plugin_api::Expression; use super::*; + #[test] + fn test_format_key_name() { + assert_eq!( + format_key_name("avg_file_size_bytes"), + "Avg File Size Bytes" + ); + assert_eq!(format_key_name("total_media"), "Total Media"); + assert_eq!(format_key_name("id"), "Id"); + assert_eq!(format_key_name(""), ""); + } + #[test] fn test_extract_cell_string() { let row = serde_json::json!({ "name": "Alice", "count": 5 }); From 24b31bfeb9da27ce783dc5f94212036f2c06ca56 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:27:42 +0300 Subject: [PATCH 2/8] pinakes-ui: integrate plugin pages into sidebar navigation; sanitize theme-extension CSS eval Signed-off-by: NotAShelf Change-Id: Ie87e39c66253a7071f029d52dd5979716a6a6964 --- crates/pinakes-ui/src/app.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/pinakes-ui/src/app.rs b/crates/pinakes-ui/src/app.rs index 11026b9..43699d2 100644 --- a/crates/pinakes-ui/src/app.rs +++ b/crates/pinakes-ui/src/app.rs @@ -369,11 +369,13 @@ pub fn App() -> Element { spawn(async move { let js: String = vars .iter() - .map(|(k, v)| { - format!( - "document.documentElement.style.setProperty('{}','{}');", - k, v - ) + .filter_map(|(k, v)| { + let k_js = serde_json::to_string(k).ok()?; + let v_js = serde_json::to_string(v).ok()?; + Some(format!( + "document.documentElement.style.setProperty({k_js},\ + {v_js});" + )) }) .collect(); let _ = document::eval(&js).await; @@ -849,17 +851,6 @@ pub fn App() -> Element { } } } - { - let sync_time_opt = plugin_registry - .read() - .last_refresh() - .map(|ts| ts.format("%H:%M").to_string()); - rsx! { - if let Some(sync_time) = sync_time_opt { - div { class: "nav-sync-time", "Synced {sync_time}" } - } - } - } } } From c25617670a1e8a8a3c8355671a762c726d92b48c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:27:55 +0300 Subject: [PATCH 3/8] examples/media-stats-ui: fix Transform source key; add file_name column Signed-off-by: NotAShelf Change-Id: I4c741e4b36708f2078fed8154d7341de6a6a6964 --- examples/plugins/media-stats-ui/pages/stats.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/plugins/media-stats-ui/pages/stats.json b/examples/plugins/media-stats-ui/pages/stats.json index 03961a0..6f860c5 100644 --- a/examples/plugins/media-stats-ui/pages/stats.json +++ b/examples/plugins/media-stats-ui/pages/stats.json @@ -64,6 +64,10 @@ "filterable": true, "page_size": 10, "columns": [ + { + "key": "file_name", + "header": "Filename" + }, { "key": "title", "header": "Title" @@ -120,13 +124,9 @@ "path": "/api/v1/media" }, "type-breakdown": { - "type": "static", - "value": [ - { "type": "Audio", "count": 0 }, - { "type": "Video", "count": 0 }, - { "type": "Image", "count": 0 }, - { "type": "Document", "count": 0 } - ] + "type": "transform", + "source": "stats", + "expression": "stats.media_by_type" } } } From a80359343bc91188fb80c12395d881589dc5696b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:29:24 +0300 Subject: [PATCH 4/8] chore: fix clippy lints; format Signed-off-by: NotAShelf Change-Id: Ib3d98a81c7e41054d27e617394bef63c6a6a6964 --- crates/pinakes-core/src/thumbnail.rs | 5 ++--- crates/pinakes-server/src/dto/media.rs | 9 +++++---- examples/plugins/media-stats-ui/plugin.toml | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/pinakes-core/src/thumbnail.rs b/crates/pinakes-core/src/thumbnail.rs index e221c76..7e3b799 100644 --- a/crates/pinakes-core/src/thumbnail.rs +++ b/crates/pinakes-core/src/thumbnail.rs @@ -27,11 +27,10 @@ impl TempFileGuard { impl Drop for TempFileGuard { fn drop(&mut self) { - if self.0.exists() { - if let Err(e) = std::fs::remove_file(&self.0) { + if self.0.exists() + && let Err(e) = std::fs::remove_file(&self.0) { warn!("failed to clean up temp file {}: {e}", self.0.display()); } - } } } diff --git a/crates/pinakes-server/src/dto/media.rs b/crates/pinakes-server/src/dto/media.rs index dc1a155..e404776 100644 --- a/crates/pinakes-server/src/dto/media.rs +++ b/crates/pinakes-server/src/dto/media.rs @@ -11,19 +11,20 @@ use uuid::Uuid; /// forward-slash-separated relative path string. Falls back to the full path /// string when no root matches. If `roots` is empty, returns the full path as a /// string so internal callers that have not yet migrated still work. +#[must_use] pub fn relativize_path(full_path: &Path, roots: &[PathBuf]) -> String { let mut best: Option<&PathBuf> = None; for root in roots { if full_path.starts_with(root) { let is_longer = best - .map_or(true, |b| root.components().count() > b.components().count()); + .is_none_or(|b| root.components().count() > b.components().count()); if is_longer { best = Some(root); } } } - if let Some(root) = best { - if let Ok(rel) = full_path.strip_prefix(root) { + if let Some(root) = best + && let Ok(rel) = full_path.strip_prefix(root) { // Normalise to forward slashes on all platforms. return rel .components() @@ -31,7 +32,6 @@ pub fn relativize_path(full_path: &Path, roots: &[PathBuf]) -> String { .collect::>() .join("/"); } - } full_path.to_string_lossy().into_owned() } @@ -269,6 +269,7 @@ impl MediaResponse { /// matching root prefix from the path before serialization. Pass the /// configured root directories so that clients receive a relative path /// (e.g. `"Music/song.mp3"`) rather than a full server filesystem path. + #[must_use] pub fn new(item: pinakes_core::model::MediaItem, roots: &[PathBuf]) -> Self { Self { id: item.id.0.to_string(), diff --git a/examples/plugins/media-stats-ui/plugin.toml b/examples/plugins/media-stats-ui/plugin.toml index f65def5..0e8116a 100644 --- a/examples/plugins/media-stats-ui/plugin.toml +++ b/examples/plugins/media-stats-ui/plugin.toml @@ -9,7 +9,7 @@ license = "EUPL-1.2" kind = ["ui_page"] [plugin.binary] -wasm = "media_stats_ui.wasm" +wasm = "target/wasm32-unknown-unknown/release/media_stats_ui.wasm" [capabilities] network = false @@ -19,7 +19,7 @@ read = [] write = [] [ui] -required_endpoints = ["/api/v1/statistics", "/api/v1/media"] +required_endpoints = ["/api/v1/statistics", "/api/v1/media", "/api/v1/tags"] # UI pages [[ui.pages]] From 7b841cbd9ad347292590ec02ae2cbf19b8aea0cd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:26:59 +0300 Subject: [PATCH 5/8] pinakes-ui: supply `local_state` to `Conditional` and `Progress`; remove `last_refresh` Signed-off-by: NotAShelf Change-Id: Ib513b5846d6c74bfe821da195b7080af6a6a6964 --- crates/pinakes-ui/src/plugin_ui/registry.rs | 18 ++---- crates/pinakes-ui/src/plugin_ui/renderer.rs | 64 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/crates/pinakes-ui/src/plugin_ui/registry.rs b/crates/pinakes-ui/src/plugin_ui/registry.rs index 4ad2b5c..8fde3d0 100644 --- a/crates/pinakes-ui/src/plugin_ui/registry.rs +++ b/crates/pinakes-ui/src/plugin_ui/registry.rs @@ -41,15 +41,13 @@ pub struct PluginPage { #[derive(Debug, Clone)] pub struct PluginRegistry { /// API client for fetching pages from server - client: ApiClient, + client: ApiClient, /// Cached pages: (`plugin_id`, `page_id`) -> `PluginPage` - pages: HashMap<(String, String), PluginPage>, + pages: HashMap<(String, String), PluginPage>, /// Cached widgets: (`plugin_id`, `widget_id`) -> `UiWidget` - widgets: Vec<(String, UiWidget)>, + widgets: Vec<(String, UiWidget)>, /// Merged CSS custom property overrides from all enabled plugins - theme_vars: HashMap, - /// Last refresh timestamp - last_refresh: Option>, + theme_vars: HashMap, } impl PluginRegistry { @@ -60,7 +58,6 @@ impl PluginRegistry { pages: HashMap::new(), widgets: Vec::new(), theme_vars: HashMap::new(), - last_refresh: None, } } @@ -206,14 +203,8 @@ impl PluginRegistry { self.pages = tmp.pages; self.widgets = tmp.widgets; self.theme_vars = tmp.theme_vars; - self.last_refresh = Some(chrono::Utc::now()); Ok(()) } - - /// Get last refresh time - pub const fn last_refresh(&self) -> Option> { - self.last_refresh - } } impl Default for PluginRegistry { @@ -346,7 +337,6 @@ mod tests { let registry = PluginRegistry::default(); assert!(registry.is_empty()); assert_eq!(registry.all_pages().len(), 0); - assert!(registry.last_refresh().is_none()); } #[test] diff --git a/crates/pinakes-ui/src/plugin_ui/renderer.rs b/crates/pinakes-ui/src/plugin_ui/renderer.rs index 0272e6b..fa62f65 100644 --- a/crates/pinakes-ui/src/plugin_ui/renderer.rs +++ b/crates/pinakes-ui/src/plugin_ui/renderer.rs @@ -708,7 +708,8 @@ pub fn render_element( } else if let Some(arr) = items.and_then(|v| v.as_array()) { for item in arr { { - let url_opt = media_grid_image_url(item); + let base = ctx.client.peek().base_url().to_string(); + let url_opt = media_grid_image_url(item, &base); let label = media_grid_label(item); rsx! { div { class: "media-grid-item", @@ -795,7 +796,16 @@ pub fn render_element( .map(|obj| { obj .iter() - .map(|(k, v)| (k.clone(), value_to_display_string(v))) + .filter_map(|(k, v)| { + match v { + // Skip nested objects and arrays; they are not meaningful as + // single-line description terms. + serde_json::Value::Object(_) | serde_json::Value::Array(_) => { + None + }, + _ => Some((format_key_name(k), value_to_display_string(v))), + } + }) .collect() }) .unwrap_or_default(); @@ -1044,7 +1054,7 @@ pub fn render_element( max, show_percentage, } => { - let eval_ctx = data.as_json(); + let eval_ctx = build_ctx(data, &ctx.local_state.read()); let pct = evaluate_expression_as_f64(value, &eval_ctx); let fraction = if *max > 0.0 { (pct / max).clamp(0.0, 1.0) @@ -1116,7 +1126,7 @@ pub fn render_element( then, else_element, } => { - let eval_ctx = data.as_json(); + let eval_ctx = build_ctx(data, &ctx.local_state.read()); if evaluate_expression_as_bool(condition, &eval_ctx) { render_element(then, data, actions, ctx) } else if let Some(else_el) = else_element { @@ -1244,7 +1254,10 @@ fn render_chart_data( // MediaGrid helpers /// Probe a JSON object for common image URL fields. -fn media_grid_image_url(item: &serde_json::Value) -> Option { +fn media_grid_image_url( + item: &serde_json::Value, + base_url: &str, +) -> Option { for key in &[ "thumbnail_url", "thumbnail", @@ -1260,12 +1273,22 @@ fn media_grid_image_url(item: &serde_json::Value) -> Option { } } } + // Pinakes media items: construct absolute thumbnail URL from id when + // has_thumbnail is true. Relative paths don't work for in the + // desktop WebView context. + if item.get("has_thumbnail").and_then(|v| v.as_bool()) == Some(true) { + if let Some(id) = item.get("id").and_then(|v| v.as_str()) { + if !id.is_empty() { + return Some(format!("{base_url}/api/v1/media/{id}/thumbnail")); + } + } + } None } /// Probe a JSON object for a human-readable label. fn media_grid_label(item: &serde_json::Value) -> String { - for key in &["title", "name", "label", "caption"] { + for key in &["title", "name", "label", "caption", "file_name"] { if let Some(s) = item.get(*key).and_then(|v| v.as_str()) { if !s.is_empty() { return s.to_string(); @@ -1601,12 +1624,41 @@ fn safe_col_width_css(w: &str) -> Option { None } +/// Convert a `snake_case` JSON key to a human-readable title. +/// `avg_file_size_bytes` -> `Avg File Size Bytes` +fn format_key_name(key: &str) -> String { + key + .split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => { + first.to_uppercase().collect::() + chars.as_str() + }, + } + }) + .collect::>() + .join(" ") +} + #[cfg(test)] mod tests { use pinakes_plugin_api::Expression; use super::*; + #[test] + fn test_format_key_name() { + assert_eq!( + format_key_name("avg_file_size_bytes"), + "Avg File Size Bytes" + ); + assert_eq!(format_key_name("total_media"), "Total Media"); + assert_eq!(format_key_name("id"), "Id"); + assert_eq!(format_key_name(""), ""); + } + #[test] fn test_extract_cell_string() { let row = serde_json::json!({ "name": "Alice", "count": 5 }); From 6d42ac0f48484970958824476708423903e102e3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:27:42 +0300 Subject: [PATCH 6/8] pinakes-ui: integrate plugin pages into sidebar navigation; sanitize theme-extension CSS eval Signed-off-by: NotAShelf Change-Id: Ie87e39c66253a7071f029d52dd5979716a6a6964 --- crates/pinakes-ui/src/app.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/pinakes-ui/src/app.rs b/crates/pinakes-ui/src/app.rs index 11026b9..43699d2 100644 --- a/crates/pinakes-ui/src/app.rs +++ b/crates/pinakes-ui/src/app.rs @@ -369,11 +369,13 @@ pub fn App() -> Element { spawn(async move { let js: String = vars .iter() - .map(|(k, v)| { - format!( - "document.documentElement.style.setProperty('{}','{}');", - k, v - ) + .filter_map(|(k, v)| { + let k_js = serde_json::to_string(k).ok()?; + let v_js = serde_json::to_string(v).ok()?; + Some(format!( + "document.documentElement.style.setProperty({k_js},\ + {v_js});" + )) }) .collect(); let _ = document::eval(&js).await; @@ -849,17 +851,6 @@ pub fn App() -> Element { } } } - { - let sync_time_opt = plugin_registry - .read() - .last_refresh() - .map(|ts| ts.format("%H:%M").to_string()); - rsx! { - if let Some(sync_time) = sync_time_opt { - div { class: "nav-sync-time", "Synced {sync_time}" } - } - } - } } } From 9176b764b09285d70af135e12322a6a95c015dac Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:27:55 +0300 Subject: [PATCH 7/8] examples/media-stats-ui: fix Transform source key; add file_name column Signed-off-by: NotAShelf Change-Id: I4c741e4b36708f2078fed8154d7341de6a6a6964 --- examples/plugins/media-stats-ui/pages/stats.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/plugins/media-stats-ui/pages/stats.json b/examples/plugins/media-stats-ui/pages/stats.json index 03961a0..6f860c5 100644 --- a/examples/plugins/media-stats-ui/pages/stats.json +++ b/examples/plugins/media-stats-ui/pages/stats.json @@ -64,6 +64,10 @@ "filterable": true, "page_size": 10, "columns": [ + { + "key": "file_name", + "header": "Filename" + }, { "key": "title", "header": "Title" @@ -120,13 +124,9 @@ "path": "/api/v1/media" }, "type-breakdown": { - "type": "static", - "value": [ - { "type": "Audio", "count": 0 }, - { "type": "Video", "count": 0 }, - { "type": "Image", "count": 0 }, - { "type": "Document", "count": 0 } - ] + "type": "transform", + "source": "stats", + "expression": "stats.media_by_type" } } } From b6e579408f181fdd3560888d72a9f03e2670c9f6 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:29:24 +0300 Subject: [PATCH 8/8] chore: fix clippy lints; format Signed-off-by: NotAShelf Change-Id: Ib3d98a81c7e41054d27e617394bef63c6a6a6964 --- crates/pinakes-core/src/thumbnail.rs | 5 ++--- crates/pinakes-server/src/dto/media.rs | 9 +++++---- examples/plugins/media-stats-ui/plugin.toml | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/pinakes-core/src/thumbnail.rs b/crates/pinakes-core/src/thumbnail.rs index e221c76..7e3b799 100644 --- a/crates/pinakes-core/src/thumbnail.rs +++ b/crates/pinakes-core/src/thumbnail.rs @@ -27,11 +27,10 @@ impl TempFileGuard { impl Drop for TempFileGuard { fn drop(&mut self) { - if self.0.exists() { - if let Err(e) = std::fs::remove_file(&self.0) { + if self.0.exists() + && let Err(e) = std::fs::remove_file(&self.0) { warn!("failed to clean up temp file {}: {e}", self.0.display()); } - } } } diff --git a/crates/pinakes-server/src/dto/media.rs b/crates/pinakes-server/src/dto/media.rs index dc1a155..e404776 100644 --- a/crates/pinakes-server/src/dto/media.rs +++ b/crates/pinakes-server/src/dto/media.rs @@ -11,19 +11,20 @@ use uuid::Uuid; /// forward-slash-separated relative path string. Falls back to the full path /// string when no root matches. If `roots` is empty, returns the full path as a /// string so internal callers that have not yet migrated still work. +#[must_use] pub fn relativize_path(full_path: &Path, roots: &[PathBuf]) -> String { let mut best: Option<&PathBuf> = None; for root in roots { if full_path.starts_with(root) { let is_longer = best - .map_or(true, |b| root.components().count() > b.components().count()); + .is_none_or(|b| root.components().count() > b.components().count()); if is_longer { best = Some(root); } } } - if let Some(root) = best { - if let Ok(rel) = full_path.strip_prefix(root) { + if let Some(root) = best + && let Ok(rel) = full_path.strip_prefix(root) { // Normalise to forward slashes on all platforms. return rel .components() @@ -31,7 +32,6 @@ pub fn relativize_path(full_path: &Path, roots: &[PathBuf]) -> String { .collect::>() .join("/"); } - } full_path.to_string_lossy().into_owned() } @@ -269,6 +269,7 @@ impl MediaResponse { /// matching root prefix from the path before serialization. Pass the /// configured root directories so that clients receive a relative path /// (e.g. `"Music/song.mp3"`) rather than a full server filesystem path. + #[must_use] pub fn new(item: pinakes_core::model::MediaItem, roots: &[PathBuf]) -> Self { Self { id: item.id.0.to_string(), diff --git a/examples/plugins/media-stats-ui/plugin.toml b/examples/plugins/media-stats-ui/plugin.toml index f65def5..0e8116a 100644 --- a/examples/plugins/media-stats-ui/plugin.toml +++ b/examples/plugins/media-stats-ui/plugin.toml @@ -9,7 +9,7 @@ license = "EUPL-1.2" kind = ["ui_page"] [plugin.binary] -wasm = "media_stats_ui.wasm" +wasm = "target/wasm32-unknown-unknown/release/media_stats_ui.wasm" [capabilities] network = false @@ -19,7 +19,7 @@ read = [] write = [] [ui] -required_endpoints = ["/api/v1/statistics", "/api/v1/media"] +required_endpoints = ["/api/v1/statistics", "/api/v1/media", "/api/v1/tags"] # UI pages [[ui.pages]]