pinakes-ui: supply local_state to Conditional and Progress; remove last_refresh

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib513b5846d6c74bfe821da195b7080af6a6a6964
This commit is contained in:
raf 2026-03-11 21:26:59 +03:00
commit 90504609e9
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 62 additions and 20 deletions

View file

@ -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<String, String>,
/// Last refresh timestamp
last_refresh: Option<chrono::DateTime<chrono::Utc>>,
theme_vars: HashMap<String, String>,
}
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<chrono::DateTime<chrono::Utc>> {
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]

View file

@ -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<String> {
fn media_grid_image_url(
item: &serde_json::Value,
base_url: &str,
) -> Option<String> {
for key in &[
"thumbnail_url",
"thumbnail",
@ -1260,12 +1273,22 @@ fn media_grid_image_url(item: &serde_json::Value) -> Option<String> {
}
}
}
// Pinakes media items: construct absolute thumbnail URL from id when
// has_thumbnail is true. Relative paths don't work for <img src> 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<String> {
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::<String>() + chars.as_str()
},
}
})
.collect::<Vec<_>>()
.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 });