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:
parent
071ea19c8f
commit
90504609e9
2 changed files with 62 additions and 20 deletions
|
|
@ -41,15 +41,13 @@ pub struct PluginPage {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PluginRegistry {
|
pub struct PluginRegistry {
|
||||||
/// API client for fetching pages from server
|
/// API client for fetching pages from server
|
||||||
client: ApiClient,
|
client: ApiClient,
|
||||||
/// Cached pages: (`plugin_id`, `page_id`) -> `PluginPage`
|
/// Cached pages: (`plugin_id`, `page_id`) -> `PluginPage`
|
||||||
pages: HashMap<(String, String), PluginPage>,
|
pages: HashMap<(String, String), PluginPage>,
|
||||||
/// Cached widgets: (`plugin_id`, `widget_id`) -> `UiWidget`
|
/// Cached widgets: (`plugin_id`, `widget_id`) -> `UiWidget`
|
||||||
widgets: Vec<(String, UiWidget)>,
|
widgets: Vec<(String, UiWidget)>,
|
||||||
/// Merged CSS custom property overrides from all enabled plugins
|
/// Merged CSS custom property overrides from all enabled plugins
|
||||||
theme_vars: HashMap<String, String>,
|
theme_vars: HashMap<String, String>,
|
||||||
/// Last refresh timestamp
|
|
||||||
last_refresh: Option<chrono::DateTime<chrono::Utc>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PluginRegistry {
|
impl PluginRegistry {
|
||||||
|
|
@ -60,7 +58,6 @@ impl PluginRegistry {
|
||||||
pages: HashMap::new(),
|
pages: HashMap::new(),
|
||||||
widgets: Vec::new(),
|
widgets: Vec::new(),
|
||||||
theme_vars: HashMap::new(),
|
theme_vars: HashMap::new(),
|
||||||
last_refresh: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,14 +203,8 @@ impl PluginRegistry {
|
||||||
self.pages = tmp.pages;
|
self.pages = tmp.pages;
|
||||||
self.widgets = tmp.widgets;
|
self.widgets = tmp.widgets;
|
||||||
self.theme_vars = tmp.theme_vars;
|
self.theme_vars = tmp.theme_vars;
|
||||||
self.last_refresh = Some(chrono::Utc::now());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get last refresh time
|
|
||||||
pub const fn last_refresh(&self) -> Option<chrono::DateTime<chrono::Utc>> {
|
|
||||||
self.last_refresh
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PluginRegistry {
|
impl Default for PluginRegistry {
|
||||||
|
|
@ -346,7 +337,6 @@ mod tests {
|
||||||
let registry = PluginRegistry::default();
|
let registry = PluginRegistry::default();
|
||||||
assert!(registry.is_empty());
|
assert!(registry.is_empty());
|
||||||
assert_eq!(registry.all_pages().len(), 0);
|
assert_eq!(registry.all_pages().len(), 0);
|
||||||
assert!(registry.last_refresh().is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -708,7 +708,8 @@ pub fn render_element(
|
||||||
} else if let Some(arr) = items.and_then(|v| v.as_array()) {
|
} else if let Some(arr) = items.and_then(|v| v.as_array()) {
|
||||||
for item in arr {
|
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);
|
let label = media_grid_label(item);
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "media-grid-item",
|
div { class: "media-grid-item",
|
||||||
|
|
@ -795,7 +796,16 @@ pub fn render_element(
|
||||||
.map(|obj| {
|
.map(|obj| {
|
||||||
obj
|
obj
|
||||||
.iter()
|
.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()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -1044,7 +1054,7 @@ pub fn render_element(
|
||||||
max,
|
max,
|
||||||
show_percentage,
|
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 pct = evaluate_expression_as_f64(value, &eval_ctx);
|
||||||
let fraction = if *max > 0.0 {
|
let fraction = if *max > 0.0 {
|
||||||
(pct / max).clamp(0.0, 1.0)
|
(pct / max).clamp(0.0, 1.0)
|
||||||
|
|
@ -1116,7 +1126,7 @@ pub fn render_element(
|
||||||
then,
|
then,
|
||||||
else_element,
|
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) {
|
if evaluate_expression_as_bool(condition, &eval_ctx) {
|
||||||
render_element(then, data, actions, ctx)
|
render_element(then, data, actions, ctx)
|
||||||
} else if let Some(else_el) = else_element {
|
} else if let Some(else_el) = else_element {
|
||||||
|
|
@ -1244,7 +1254,10 @@ fn render_chart_data(
|
||||||
// MediaGrid helpers
|
// MediaGrid helpers
|
||||||
|
|
||||||
/// Probe a JSON object for common image URL fields.
|
/// 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 &[
|
for key in &[
|
||||||
"thumbnail_url",
|
"thumbnail_url",
|
||||||
"thumbnail",
|
"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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe a JSON object for a human-readable label.
|
/// Probe a JSON object for a human-readable label.
|
||||||
fn media_grid_label(item: &serde_json::Value) -> String {
|
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 let Some(s) = item.get(*key).and_then(|v| v.as_str()) {
|
||||||
if !s.is_empty() {
|
if !s.is_empty() {
|
||||||
return s.to_string();
|
return s.to_string();
|
||||||
|
|
@ -1601,12 +1624,41 @@ fn safe_col_width_css(w: &str) -> Option<String> {
|
||||||
None
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pinakes_plugin_api::Expression;
|
use pinakes_plugin_api::Expression;
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_extract_cell_string() {
|
fn test_extract_cell_string() {
|
||||||
let row = serde_json::json!({ "name": "Alice", "count": 5 });
|
let row = serde_json::json!({ "name": "Alice", "count": 5 });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue