From 90504609e9b3350b39b5d0cb86ab79b703b3f4a2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 11 Mar 2026 21:26:59 +0300 Subject: [PATCH] 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 });