diff --git a/crates/aster-webui/assets/ricardo.gif b/crates/aster-webui/assets/ricardo.gif new file mode 100644 index 0000000..3b7487a Binary files /dev/null and b/crates/aster-webui/assets/ricardo.gif differ diff --git a/crates/aster-webui/src/display.rs b/crates/aster-webui/src/display.rs index 75ee94b..6d68815 100644 --- a/crates/aster-webui/src/display.rs +++ b/crates/aster-webui/src/display.rs @@ -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 { 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()], }; diff --git a/crates/aster-webui/src/images.rs b/crates/aster-webui/src/images.rs index 224ed04..b1f1e3f 100644 --- a/crates/aster-webui/src/images.rs +++ b/crates/aster-webui/src/images.rs @@ -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> { let mut out = Vec::new(); @@ -36,6 +39,9 @@ pub(crate) async fn list_images(state: &AppState) -> Result> { } 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 { + 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 { 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) } diff --git a/crates/aster-webui/src/monitor.rs b/crates/aster-webui/src/monitor.rs index 8f26b8e..9817858 100644 --- a/crates/aster-webui/src/monitor.rs +++ b/crates/aster-webui/src/monitor.rs @@ -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 Result Vec { .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(), diff --git a/crates/aster-webui/src/routes.rs b/crates/aster-webui/src/routes.rs index a3ceb8b..41cd684 100644 --- a/crates/aster-webui/src/routes.rs +++ b/crates/aster-webui/src/routes.rs @@ -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::() { @@ -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 { diff --git a/crates/aster-webui/src/types.rs b/crates/aster-webui/src/types.rs index ba48e1b..06a4977 100644 --- a/crates/aster-webui/src/types.rs +++ b/crates/aster-webui/src/types.rs @@ -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, } @@ -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, @@ -177,7 +178,7 @@ pub(crate) struct ActivateRequest { pub(crate) specs_enabled: Option, pub(crate) specs_mode: Option, pub(crate) memes_enabled: Option, - pub(crate) gifs_enabled: Option, + pub(crate) ricardo_enabled: Option, } #[derive(Deserialize)] diff --git a/crates/aster-webui/src/ui.rs b/crates/aster-webui/src/ui.rs index 465cdac..154cea7 100644 --- a/crates/aster-webui/src/ui.rs +++ b/crates/aster-webui/src/ui.rs @@ -145,7 +145,7 @@ const INDEX_HTML: &str = r##" .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##" 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##" 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##" .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##" 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##" Upload Source +
@@ -498,16 +524,7 @@ const INDEX_HTML: &str = r##"
- -
-
- +
@@ -573,7 +590,7 @@ const INDEX_HTML: &str = r##" 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##" 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##" 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##" 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##" 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##" 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##" tile.innerHTML = ` ${safeName}
-
${safeLabel}
-
${bytes(image.size)}${gifFrameLabel(image)}
+
${safeLabel}
+
${bytes(image.size)}${gifFrameLabel(image)}
- +
`; 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##" 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)); });