feat: add built-in ricardo display mode
Build And Push Container / build-and-push (push) Successful in 58s

This commit is contained in:
2026-06-30 23:45:45 +02:00
parent 382e82601a
commit 33967f3b7b
7 changed files with 177 additions and 101 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

+40 -39
View File
@@ -10,7 +10,10 @@ use chrono::Utc;
use tracing::{error, info, warn};
use crate::{
images::{is_gif_manifest_name, load_gif_manifest, load_panel_rgb, MIN_GIF_FRAME_DELAY_MS},
images::{
is_gif_manifest_name, load_gif_manifest, load_panel_rgb, MIN_GIF_FRAME_DELAY_MS,
RICARDO_MANIFEST_NAME,
},
monitor::{load_monitor_json_sync, rotation_snapshot},
system::{overlay_system_specs, render_system_panel},
types::{ActiveFrame, AppState, DisplayConfig, DisplayStatus, RotationSlot, RotationSnapshot},
@@ -36,7 +39,7 @@ pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
custom_panel: false,
specs_enabled: false,
memes_enabled: false,
gifs_enabled: false,
ricardo_enabled: false,
rotation_active: false,
switch_time: "10".into(),
active_images: Vec::new(),
@@ -153,7 +156,7 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
status.custom_panel = snapshot.custom_panel;
status.specs_enabled = snapshot.specs_enabled;
status.memes_enabled = snapshot.memes_enabled;
status.gifs_enabled = snapshot.gifs_enabled;
status.ricardo_enabled = snapshot.ricardo_enabled;
status.rotation_active = snapshot.rotation_active();
status.switch_time = snapshot.switch_time.to_string();
status.active_images = snapshot.active_images.clone();
@@ -239,37 +242,11 @@ fn build_rotation_slots(
image_dir: &std::path::PathBuf,
) -> Vec<RotationSlot> {
let mut slots = Vec::new();
if snapshot.memes_enabled || snapshot.gifs_enabled {
if snapshot.memes_enabled {
for name in &snapshot.active_images {
if is_gif_manifest_name(name) {
if !snapshot.gifs_enabled {
continue;
}
match load_gif_manifest(image_dir, name) {
Ok(manifest) if !manifest.frames.is_empty() => {
let mut frames = Vec::new();
for (idx, frame) in manifest.frames.iter().enumerate() {
frames.push((
ActiveFrame {
key: format!("{name}#{idx}"),
image_name: frame.name.clone(),
display_name: format!("{} [{}]", manifest.label, idx + 1),
animated: true,
},
Duration::from_millis(
frame.delay_ms.max(MIN_GIF_FRAME_DELAY_MS) as u64
),
));
}
slots.push(RotationSlot {
total_duration: Duration::from_secs(snapshot.switch_time as u64),
frames,
});
}
Ok(_) => {}
Err(err) => warn!("Skipping GIF set {name}: {err}"),
}
} else if snapshot.memes_enabled {
continue;
} else {
slots.push(RotationSlot {
total_duration: Duration::from_secs(snapshot.switch_time as u64),
frames: vec![(
@@ -281,8 +258,32 @@ fn build_rotation_slots(
},
Duration::from_secs(snapshot.switch_time as u64),
)],
})
}
}
}
if snapshot.ricardo_enabled {
match load_gif_manifest(image_dir, RICARDO_MANIFEST_NAME) {
Ok(manifest) if !manifest.frames.is_empty() => {
let mut frames = Vec::new();
for (idx, frame) in manifest.frames.iter().enumerate() {
frames.push((
ActiveFrame {
key: format!("{RICARDO_MANIFEST_NAME}#{idx}"),
image_name: frame.name.clone(),
display_name: format!("{} [{}]", manifest.label, idx + 1),
animated: true,
},
Duration::from_millis(frame.delay_ms.max(MIN_GIF_FRAME_DELAY_MS) as u64),
));
}
slots.push(RotationSlot {
total_duration: Duration::from_secs(snapshot.switch_time as u64),
frames,
});
}
Ok(_) => {}
Err(err) => warn!("Skipping Ricardo GIF: {err}"),
}
}
if slots.is_empty() && snapshot.specs_enabled {
@@ -633,14 +634,14 @@ mod tests {
}
#[test]
fn build_rotation_slots_uses_switch_time_for_gif_slot_duration() {
fn build_rotation_slots_uses_switch_time_for_ricardo_slot_duration() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-test-{unique}"));
fs::create_dir_all(&image_dir).unwrap();
let manifest_name = "panel-test.gifset.json";
let manifest_name = RICARDO_MANIFEST_NAME;
fs::write(
image_dir.join(manifest_name),
r#"{
@@ -662,8 +663,8 @@ mod tests {
specs_enabled: false,
specs_mode: "cards".into(),
memes_enabled: false,
gifs_enabled: true,
active_images: vec![manifest_name.into()],
ricardo_enabled: true,
active_images: Vec::new(),
};
let slots = build_rotation_slots(&snapshot, &image_dir);
@@ -675,7 +676,7 @@ mod tests {
}
#[test]
fn build_rotation_slots_skips_gif_when_gifs_are_disabled() {
fn build_rotation_slots_skips_uploaded_gif_when_only_memes_are_enabled() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@@ -704,8 +705,8 @@ mod tests {
switch_time: 7,
specs_enabled: false,
specs_mode: "cards".into(),
memes_enabled: false,
gifs_enabled: false,
memes_enabled: true,
ricardo_enabled: false,
active_images: vec![manifest_name.into()],
};
+47 -3
View File
@@ -15,9 +15,12 @@ use crate::{
};
pub(crate) const NYAN_CAT_GIF: &[u8] = include_bytes!("../assets/nyan-cat.gif");
pub(crate) const RICARDO_GIF: &[u8] = include_bytes!("../assets/ricardo.gif");
pub(crate) const MIN_GIF_FRAME_DELAY_MS: u32 = 33;
const GIF_FRAME_PREFIX: &str = ".gifframe-";
const GIF_MANIFEST_SUFFIX: &str = ".gifset.json";
pub(crate) const RICARDO_MANIFEST_NAME: &str = "builtin-ricardo.gifset.json";
const RICARDO_FRAME_STEM: &str = "builtin-ricardo";
pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
let mut out = Vec::new();
@@ -36,6 +39,9 @@ pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
}
let meta = entry.metadata().await?;
if is_gif_manifest_name(&name) {
if is_builtin_ricardo_manifest_name(&name) {
continue;
}
let raw = fs::read_to_string(&path).await?;
let manifest: GifSetManifest = serde_json::from_str(&raw)
.with_context(|| format!("Failed to parse GIF manifest {}", path.display()))?;
@@ -85,6 +91,33 @@ pub(crate) async fn convert_and_store_image(
}
async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8]) -> Result<String> {
store_gif_frames(state, file_name, bytes, None, None).await
}
pub(crate) async fn ensure_builtin_ricardo_gif(state: &AppState) -> Result<()> {
let manifest_path = state.image_dir.join(RICARDO_MANIFEST_NAME);
if fs::try_exists(&manifest_path).await? {
return Ok(());
}
store_gif_frames(
state,
"ricardo.gif",
RICARDO_GIF,
Some(RICARDO_MANIFEST_NAME),
Some(RICARDO_FRAME_STEM),
)
.await
.map(|_| ())
}
async fn store_gif_frames(
state: &AppState,
file_name: &str,
bytes: &[u8],
manifest_name_override: Option<&str>,
frame_stem_override: Option<&str>,
) -> Result<String> {
let decoder = GifDecoder::new(Cursor::new(bytes))
.with_context(|| format!("Unsupported GIF format: {file_name}"))?;
let frames = decoder
@@ -96,9 +129,12 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
}
let source_frame_count = frames.len();
let base = make_image_stem(file_name);
let base = frame_stem_override.unwrap_or(file_name);
let clean_base = make_image_stem(base);
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
let manifest_name = format!("panel-{timestamp}-{base}{GIF_MANIFEST_SUFFIX}");
let manifest_name = manifest_name_override
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("panel-{timestamp}-{clean_base}{GIF_MANIFEST_SUFFIX}"));
let mut rendered_frames = Vec::new();
for frame in frames {
@@ -116,7 +152,11 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
let mut stored_frames = Vec::new();
for (idx, (rendered, delay_ms)) in collapsed_frames.into_iter().enumerate() {
let frame_name = format!("{GIF_FRAME_PREFIX}{timestamp}-{base}-{idx:03}.jpg");
let frame_name = if manifest_name_override.is_some() {
format!("{GIF_FRAME_PREFIX}{clean_base}-{idx:03}.jpg")
} else {
format!("{GIF_FRAME_PREFIX}{timestamp}-{clean_base}-{idx:03}.jpg")
};
let frame_path = state.image_dir.join(&frame_name);
rendered
.save_with_format(&frame_path, ImageFormat::Jpeg)
@@ -210,6 +250,10 @@ pub(crate) fn is_gif_manifest_name(name: &str) -> bool {
name.ends_with(GIF_MANIFEST_SUFFIX)
}
pub(crate) fn is_builtin_ricardo_manifest_name(name: &str) -> bool {
name == RICARDO_MANIFEST_NAME
}
fn is_gif_frame_name(name: &str) -> bool {
name.starts_with(GIF_FRAME_PREFIX)
}
+24 -13
View File
@@ -6,13 +6,14 @@ use tokio::fs;
use crate::{
display::read_display_status,
images::list_images,
images::{ensure_builtin_ricardo_gif, list_images},
system::collect_system_view,
types::{AppState, RotationSnapshot, SetupView, StateResponse},
};
pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
fs::create_dir_all(&state.image_dir).await?;
ensure_builtin_ricardo_gif(state).await?;
if fs::try_exists(&state.monitor_path).await? {
return Ok(());
@@ -34,7 +35,7 @@ pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
"nativeSpecs": false,
"nativeSpecsMode": "cards",
"nativeMemes": true,
"nativeGifs": true,
"nativeRicardo": false,
"operationMode": 0,
"theme": 1,
"diskUpdate": 300,
@@ -96,7 +97,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
let specs_enabled = current_specs_enabled(&monitor);
let specs_mode = current_specs_mode(&monitor);
let memes_enabled = current_memes_enabled(&monitor);
let gifs_enabled = current_gifs_enabled(&monitor);
let ricardo_enabled = current_ricardo_enabled(&monitor);
Ok(StateResponse {
monitor_path: state.monitor_path.display().to_string(),
@@ -107,7 +108,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
specs_enabled,
specs_mode,
memes_enabled,
gifs_enabled,
ricardo_enabled,
},
active_images: current_active_images(&monitor),
images: list_images(state).await?,
@@ -123,6 +124,7 @@ pub(crate) fn current_active_images(monitor: &Value) -> Vec<String> {
.into_iter()
.flatten()
.filter_map(|panel| panel.get("img").and_then(Value::as_str))
.filter(|name| *name != crate::images::RICARDO_MANIFEST_NAME)
.map(ToString::to_string)
.collect()
}
@@ -175,13 +177,17 @@ pub(crate) fn current_specs_mode(monitor: &Value) -> String {
.unwrap_or_else(|| "cards".into())
}
pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool {
pub(crate) fn current_ricardo_enabled(monitor: &Value) -> bool {
monitor
.get("setup")
.and_then(Value::as_object)
.and_then(|setup| setup.get("nativeGifs"))
.and_then(|setup| {
setup
.get("nativeRicardo")
.or_else(|| setup.get("nativeGifs"))
})
.and_then(Value::as_bool)
.unwrap_or(true)
.unwrap_or(false)
}
pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
@@ -191,7 +197,7 @@ pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
specs_enabled: current_specs_enabled(monitor),
specs_mode: current_specs_mode(monitor),
memes_enabled: current_memes_enabled(monitor),
gifs_enabled: current_gifs_enabled(monitor),
ricardo_enabled: current_ricardo_enabled(monitor),
active_images: current_active_images(monitor),
}
}
@@ -202,10 +208,10 @@ pub(crate) fn set_custom_panels(
specs_enabled: bool,
specs_mode: &str,
memes_enabled: bool,
gifs_enabled: bool,
ricardo_enabled: bool,
images: &[String],
) {
let enabled = specs_enabled || ((memes_enabled || gifs_enabled) && !images.is_empty());
let enabled = specs_enabled || (memes_enabled && !images.is_empty()) || ricardo_enabled;
let setup = monitor
.as_object_mut()
.expect("monitor config must be an object")
@@ -221,16 +227,21 @@ pub(crate) fn set_custom_panels(
Value::String(normalize_specs_mode(specs_mode)),
);
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled));
setup.insert("nativeRicardo".into(), Value::Bool(ricardo_enabled));
}
let mut diy_images = images.to_vec();
if ricardo_enabled {
diy_images.push(crate::images::RICARDO_MANIFEST_NAME.into());
}
monitor["mianban"] = Value::Array(
(1..=images.len())
(1..=diy_images.len())
.map(|index| Value::Number((index as u64).into()))
.collect(),
);
monitor["diy"] = Value::Array(
images
diy_images
.iter()
.map(|name| json!({"type": 5, "img": name, "sensor": []}))
.collect(),
+15 -16
View File
@@ -13,13 +13,13 @@ use tokio::fs;
use crate::{
images::{
convert_and_store_image, delete_image_asset, is_gif_manifest_name, list_images,
NYAN_CAT_GIF,
convert_and_store_image, delete_image_asset, is_builtin_ricardo_manifest_name,
is_gif_manifest_name, list_images, NYAN_CAT_GIF,
},
monitor::{
build_state_response, current_active_images, current_gifs_enabled, current_memes_enabled,
current_specs_enabled, current_specs_mode, current_switch_time, load_monitor_json,
save_monitor_json, set_custom_panels,
build_state_response, current_active_images, current_memes_enabled,
current_ricardo_enabled, current_specs_enabled, current_specs_mode, current_switch_time,
load_monitor_json, save_monitor_json, set_custom_panels,
},
types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse},
ui::index_response,
@@ -103,6 +103,7 @@ async fn api_activate(
) -> Response {
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
let specs_enabled = payload.specs_enabled.unwrap_or(false);
let ricardo_enabled = payload.ricardo_enabled.unwrap_or(false);
let specs_mode = payload
.specs_mode
.as_deref()
@@ -128,17 +129,12 @@ async fn api_activate(
}
let has_static_selection = valid.iter().any(|name| !is_gif_manifest_name(name));
let has_gif_selection = valid.iter().any(|name| is_gif_manifest_name(name));
let memes_enabled = payload.memes_enabled.unwrap_or(has_static_selection);
let gifs_enabled = payload.gifs_enabled.unwrap_or(has_gif_selection);
if !specs_enabled
&& !(memes_enabled && has_static_selection)
&& !(gifs_enabled && has_gif_selection)
{
if !specs_enabled && !(memes_enabled && has_static_selection) && !ricardo_enabled {
return error_response(
StatusCode::BAD_REQUEST,
"Enable specs, memes with a still image, or GIFs with an animated image",
"Enable specs, memes with a still image, or Ricardo",
);
}
@@ -153,7 +149,7 @@ async fn api_activate(
specs_enabled,
&specs_mode,
memes_enabled,
gifs_enabled,
ricardo_enabled,
&valid,
);
@@ -167,7 +163,7 @@ async fn api_activate(
"specsEnabled": specs_enabled,
"specsMode": specs_mode,
"memesEnabled": memes_enabled,
"gifsEnabled": gifs_enabled,
"ricardoEnabled": ricardo_enabled,
"activeImages": valid,
}))
.into_response()
@@ -206,6 +202,9 @@ async fn api_delete(
if payload.name.contains('/') || payload.name.contains('\\') {
return error_response(StatusCode::BAD_REQUEST, "Invalid file name");
}
if is_builtin_ricardo_manifest_name(&payload.name) {
return error_response(StatusCode::BAD_REQUEST, "Ricardo is a built-in asset");
}
if let Err(err) = delete_image_asset(&state, &payload.name).await {
return match err.downcast_ref::<std::io::Error>() {
@@ -226,7 +225,7 @@ async fn api_delete(
let specs_enabled = current_specs_enabled(&monitor);
let specs_mode = current_specs_mode(&monitor);
let memes_enabled = current_memes_enabled(&monitor);
let gifs_enabled = current_gifs_enabled(&monitor);
let ricardo_enabled = current_ricardo_enabled(&monitor);
let before = active_images.len();
active_images.retain(|name| name != &payload.name);
if active_images.len() != before {
@@ -236,7 +235,7 @@ async fn api_delete(
specs_enabled,
&specs_mode,
memes_enabled,
gifs_enabled,
ricardo_enabled,
&active_images,
);
if let Err(err) = save_monitor_json(&state, &monitor).await {
+6 -5
View File
@@ -49,7 +49,7 @@ pub(crate) struct RotationSnapshot {
pub(crate) specs_enabled: bool,
pub(crate) specs_mode: String,
pub(crate) memes_enabled: bool,
pub(crate) gifs_enabled: bool,
pub(crate) ricardo_enabled: bool,
pub(crate) active_images: Vec<String>,
}
@@ -57,7 +57,8 @@ impl RotationSnapshot {
pub(crate) fn rotation_active(&self) -> bool {
self.custom_panel
&& (self.specs_enabled
|| ((self.memes_enabled || self.gifs_enabled) && !self.active_images.is_empty()))
|| (self.memes_enabled && !self.active_images.is_empty())
|| self.ricardo_enabled)
}
}
@@ -110,7 +111,7 @@ pub(crate) struct SetupView {
pub(crate) specs_enabled: bool,
pub(crate) specs_mode: String,
pub(crate) memes_enabled: bool,
pub(crate) gifs_enabled: bool,
pub(crate) ricardo_enabled: bool,
}
#[derive(Clone, Serialize)]
@@ -133,7 +134,7 @@ pub(crate) struct DisplayStatus {
pub(crate) custom_panel: bool,
pub(crate) specs_enabled: bool,
pub(crate) memes_enabled: bool,
pub(crate) gifs_enabled: bool,
pub(crate) ricardo_enabled: bool,
pub(crate) rotation_active: bool,
pub(crate) switch_time: String,
pub(crate) active_images: Vec<String>,
@@ -177,7 +178,7 @@ pub(crate) struct ActivateRequest {
pub(crate) specs_enabled: Option<bool>,
pub(crate) specs_mode: Option<String>,
pub(crate) memes_enabled: Option<bool>,
pub(crate) gifs_enabled: Option<bool>,
pub(crate) ricardo_enabled: Option<bool>,
}
#[derive(Deserialize)]
+45 -25
View File
@@ -145,7 +145,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
.subtle { color: var(--faint); font-size: .82rem; }
.controls {
display: grid;
grid-template-columns: 132px minmax(220px, 1fr) auto;
grid-template-columns: 132px minmax(220px, 1fr) 180px auto;
gap: 10px;
align-items: end;
}
@@ -197,6 +197,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
margin-top: 12px;
}
.select-wrap select {
width: 100%;
min-height: 42px;
border-radius: 8px;
border: 1px solid var(--line);
@@ -304,13 +305,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
display: grid;
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
gap: 10px;
align-items: stretch;
}
.tile {
display: grid;
grid-template-rows: auto 1fr;
border: 1px solid var(--line-soft);
border-radius: 8px;
overflow: hidden;
background: #0b1016;
min-width: 0;
min-height: 246px;
}
.tile.selected {
border-color: var(--accent-2);
@@ -327,11 +332,16 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
.tile-body {
padding: 10px;
display: grid;
grid-template-rows: 4.7em 2.2em 1fr auto;
gap: 9px;
min-height: 0;
}
.tile-title {
min-height: 2.5em;
line-height: 1.28;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
color: var(--text);
}
@@ -339,8 +349,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
color: var(--faint);
font-size: .82rem;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mini {
display: flex;
gap: 7px;
flex-wrap: wrap;
align-items: center;
align-self: end;
}
.mini { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
.mini button {
min-height: 34px;
padding: 7px 10px;
@@ -491,6 +510,13 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
<span>Upload Source</span>
<input id="upload" type="file" accept="image/*">
</label>
<label class="select-wrap">
<span>Specs View</span>
<select id="specsMode">
<option value="cards">Cards</option>
<option value="meters">Htop Style</option>
</select>
</label>
<div class="actions">
<button id="disable" class="danger" type="button">Disable</button>
</div>
@@ -498,16 +524,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
<div class="toggle-row">
<label class="toggle-pill"><input id="specsEnabled" type="checkbox"> System Specs</label>
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes</label>
<label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs</label>
</div>
<div class="toggle-row">
<label class="select-wrap">
<span>Screen Layout</span>
<select id="specsMode">
<option value="cards">Cards</option>
<option value="meters">Meters</option>
</select>
</label>
<label class="toggle-pill"><input id="ricardoEnabled" type="checkbox"> Ricardo</label>
</div>
</div>
</div>
@@ -573,7 +590,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
const state = {
images: [],
activeImages: [],
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, specs_mode: "cards", memes_enabled: false, gifs_enabled: true },
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, specs_mode: "cards", memes_enabled: false, ricardo_enabled: false },
display: {},
system: {},
activeTab: "screen-control",
@@ -588,7 +605,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
specsEnabled: document.getElementById("specsEnabled"),
specsMode: document.getElementById("specsMode"),
memesEnabled: document.getElementById("memesEnabled"),
gifsEnabled: document.getElementById("gifsEnabled"),
ricardoEnabled: document.getElementById("ricardoEnabled"),
customPanelValue: document.getElementById("customPanelValue"),
imageCountValue: document.getElementById("imageCountValue"),
displayStatusValue: document.getElementById("displayStatusValue"),
@@ -732,7 +749,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
specs_enabled: el.specsEnabled.checked,
specs_mode: el.specsMode.value || data.setup.specs_mode || "cards",
memes_enabled: el.memesEnabled.checked,
gifs_enabled: el.gifsEnabled.checked,
ricardo_enabled: el.ricardoEnabled.checked,
}
: data.setup;
state.display = data.display || {};
@@ -742,12 +759,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
el.specsEnabled.checked = !!data.setup.specs_enabled;
el.specsMode.value = data.setup.specs_mode || "cards";
el.memesEnabled.checked = !!data.setup.memes_enabled;
el.gifsEnabled.checked = data.setup.gifs_enabled !== false;
el.ricardoEnabled.checked = !!data.setup.ricardo_enabled;
}
const enabledModes = [
state.setup.specs_enabled ? "Specs" : "",
state.setup.memes_enabled ? "Memes" : "",
state.setup.gifs_enabled ? "GIFs" : "",
state.setup.ricardo_enabled ? "Ricardo" : "",
].filter(Boolean);
el.customPanelValue.textContent = state.setup.custom_panel
? (enabledModes.join(" + ") || "Active")
@@ -802,7 +819,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
specs_enabled: el.specsEnabled.checked,
specs_mode: el.specsMode.value,
memes_enabled: el.memesEnabled.checked,
gifs_enabled: el.gifsEnabled.checked,
ricardo_enabled: el.ricardoEnabled.checked,
}),
});
clearDirty();
@@ -833,7 +850,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
const imageMap = new Map(state.images.map((image) => [image.name, image]));
el.gallery.innerHTML = "";
for (const image of state.images) {
const selected = state.activeImages.includes(image.name);
const selectable = !image.animated;
const selected = selectable && state.activeImages.includes(image.name);
const tile = document.createElement("article");
const safeLabel = escapeHtml(image.label);
const safeName = escapeHtml(image.name);
@@ -842,16 +860,18 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
tile.innerHTML = `
<img src="${safeUrl}" alt="${safeName}">
<div class="tile-body">
<div class="tile-title">${safeLabel}</div>
<div class="tile-meta">${bytes(image.size)}${gifFrameLabel(image)}</div>
<div class="tile-title" title="${safeLabel}">${safeLabel}</div>
<div class="tile-meta" title="${bytes(image.size)}${gifFrameLabel(image)}">${bytes(image.size)}${gifFrameLabel(image)}</div>
<div class="mini">
<button class="ghost" type="button" title="${selected ? "Remove" : "Add"}">${selected ? "Remove" : "Add"}</button>
<button class="ghost" type="button" title="${selectable ? (selected ? "Remove" : "Add") : "Animated GIFs are handled by the Ricardo toggle"}"${selectable ? "" : " disabled"}>${selectable ? (selected ? "Remove" : "Add") : "Built-in only"}</button>
<button class="danger" type="button">Delete</button>
</div>
</div>
`;
const [toggleBtn, deleteBtn] = tile.querySelectorAll("button");
toggleBtn.addEventListener("click", () => toggle(image.name));
if (selectable) {
toggleBtn.addEventListener("click", () => toggle(image.name));
}
deleteBtn.addEventListener("click", () => removeImage(image.name));
el.gallery.appendChild(tile);
}
@@ -927,7 +947,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
el.specsEnabled.addEventListener("change", markDirty);
el.specsMode.addEventListener("change", markDirty);
el.memesEnabled.addEventListener("change", markDirty);
el.gifsEnabled.addEventListener("change", markDirty);
el.ricardoEnabled.addEventListener("change", markDirty);
el.tabButtons.forEach((button) => {
button.addEventListener("click", () => setActiveTab(button.dataset.tabTarget));
});