diff --git a/crates/aster-webui/src/images.rs b/crates/aster-webui/src/images.rs index 075914c..224ed04 100644 --- a/crates/aster-webui/src/images.rs +++ b/crates/aster-webui/src/images.rs @@ -45,7 +45,8 @@ pub(crate) async fn list_images(state: &AppState) -> Result> { label: manifest.label, size: meta.len(), animated: true, - frame_count: manifest.frames.len(), + frame_count: manifest.source_frame_count.unwrap_or(manifest.frames.len()), + display_frame_count: manifest.frames.len(), }); } else { out.push(ImageView { @@ -55,6 +56,7 @@ pub(crate) async fn list_images(state: &AppState) -> Result> { size: meta.len(), animated: false, frame_count: 1, + display_frame_count: 1, }); } } @@ -92,6 +94,7 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8]) if frames.is_empty() { anyhow::bail!("GIF contains no frames"); } + let source_frame_count = frames.len(); let base = make_image_stem(file_name); let timestamp = Local::now().format("%Y%m%d-%H%M%S"); @@ -132,6 +135,7 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8]) .first() .map(|frame| frame.name.clone()) .unwrap_or_default(), + source_frame_count: Some(source_frame_count), frames: stored_frames, }; let manifest_path = state.image_dir.join(&manifest_name); @@ -262,6 +266,9 @@ pub(crate) fn load_panel_rgb(image_dir: &PathBuf, image_name: &str) -> Result, pub(crate) frames: Vec, } @@ -117,6 +119,7 @@ pub(crate) struct ImageView { pub(crate) url: String, pub(crate) animated: bool, pub(crate) frame_count: usize, + pub(crate) display_frame_count: usize, } #[derive(Clone, Debug, Default, Serialize)] diff --git a/crates/aster-webui/src/ui.rs b/crates/aster-webui/src/ui.rs index 09368ca..c2115e8 100644 --- a/crates/aster-webui/src/ui.rs +++ b/crates/aster-webui/src/ui.rs @@ -87,6 +87,35 @@ const INDEX_HTML: &str = r##" .brand p { margin: 3px 0 0; color: var(--muted); font-size: .9rem; } .layout { display: grid; grid-template-columns: minmax(0, 1.45fr) 520px; gap: 14px; } .stack { display: grid; gap: 16px; } + .tabbar { + display: inline-flex; + gap: 6px; + margin-bottom: 14px; + padding: 4px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(10, 15, 20, .72); + } + .tab-button { + min-height: 38px; + padding: 8px 14px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted); + font-weight: 700; + } + .tab-button:hover { + transform: none; + background: rgba(20, 29, 40, .8); + border-color: transparent; + } + .tab-button.active { + background: linear-gradient(135deg, rgba(110, 231, 183, .16), rgba(185, 246, 106, .16)); + color: var(--text); + box-shadow: inset 0 0 0 1px rgba(110, 231, 183, .18); + } + .tab-panel[hidden] { display: none; } .panel, .card { background: linear-gradient(180deg, rgba(18, 25, 34, .98), rgba(13, 18, 25, .98)); border: 1px solid var(--line); @@ -238,6 +267,15 @@ const INDEX_HTML: &str = r##" grid-template-columns: minmax(0, 1.35fr) minmax(340px, .75fr); gap: 14px; } + .system-tab { + display: grid; + gap: 14px; + } + .specs-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + } .gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); @@ -352,7 +390,7 @@ const INDEX_HTML: &str = r##" height: auto; } @media (max-width: 1180px) { - .layout, .workbench { grid-template-columns: 1fr; } + .layout, .workbench, .specs-summary { grid-template-columns: 1fr; } .status-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 1024px) { @@ -388,33 +426,81 @@ const INDEX_HTML: &str = r##" -
-
-
-
-

Rotation Setup

- /config/Monitor3.json -
-
- - -
- + + +
+
+
+
+
+

Rotation Setup

+ /config/Monitor3.json +
+
+ + +
+ +
+
+
+ + +
-
-
- - -
+ +
+ +
+
+
+

Available Images

+ 960 x 376 +
+ +
+
+
+

Rotation Order

+ Live queue +
+
+

+
+
+
+ +
- - -
- -
-
-
-

Available Images

- 960 x 376 -
- -
-
-
-

Rotation Order

- Live queue -
-
-

-
+
@@ -465,11 +524,14 @@ const INDEX_HTML: &str = r##" setup: { switch_time: "10", custom_panel: false, specs_enabled: false, memes_enabled: false, gifs_enabled: true }, display: {}, system: {}, + activeTab: "screen-control", dirty: false, }; const el = { gallery: document.getElementById("gallery"), order: document.getElementById("order"), + tabButtons: Array.from(document.querySelectorAll("[data-tab-target]")), + tabPanels: Array.from(document.querySelectorAll("[data-tab-panel]")), switchTime: document.getElementById("switchTime"), specsEnabled: document.getElementById("specsEnabled"), memesEnabled: document.getElementById("memesEnabled"), @@ -481,6 +543,12 @@ const INDEX_HTML: &str = r##" displayTargetValue: document.getElementById("displayTargetValue"), displayErrorValue: document.getElementById("displayErrorValue"), configPathValue: document.getElementById("configPathValue"), + displayStatusMirror: document.getElementById("displayStatusMirror"), + displayTargetMirror: document.getElementById("displayTargetMirror"), + customPanelMirror: document.getElementById("customPanelMirror"), + currentImageMirror: document.getElementById("currentImageMirror"), + configPathMirror: document.getElementById("configPathMirror"), + imageCountMirror: document.getElementById("imageCountMirror"), cpuUsageValue: document.getElementById("cpuUsageValue"), cpuMetaValue: document.getElementById("cpuMetaValue"), memUsageValue: document.getElementById("memUsageValue"), @@ -522,6 +590,34 @@ const INDEX_HTML: &str = r##" .replaceAll("'", "'"); } + function gifFrameLabel(image) { + if (!image || !image.animated) return ""; + if (image.display_frame_count && image.display_frame_count !== image.frame_count) { + return ` • GIF • ${image.frame_count} source • ${image.display_frame_count} display`; + } + return ` • GIF • ${image.frame_count} Frames`; + } + + function orderFrameLabel(image) { + if (!image || !image.animated) return ""; + if (image.display_frame_count && image.display_frame_count !== image.frame_count) { + return `${image.frame_count} source • ${image.display_frame_count} display`; + } + return `${image.frame_count} Frames`; + } + + function setActiveTab(name) { + state.activeTab = name; + for (const button of el.tabButtons) { + const isActive = button.dataset.tabTarget === name; + button.classList.toggle("active", isActive); + button.setAttribute("aria-selected", isActive ? "true" : "false"); + } + for (const panel of el.tabPanels) { + panel.hidden = panel.dataset.tabPanel !== name; + } + } + function move(index, delta) { const target = index + delta; if (target < 0 || target >= state.activeImages.length) return; @@ -620,6 +716,12 @@ const INDEX_HTML: &str = r##" el.uptimeValue.textContent = `Uptime ${state.system.uptime || "-"}`; el.tempCpuValue.textContent = `CPU ${state.system.temperature_cpu || "-"}`; el.tempGpuValue.textContent = `GPU ${state.system.temperature_gpu || "-"}`; + el.displayStatusMirror.textContent = el.displayStatusValue.textContent; + el.displayTargetMirror.textContent = state.display.device || "-"; + el.customPanelMirror.textContent = el.customPanelValue.textContent; + el.currentImageMirror.textContent = state.display.current_image || "No active frame"; + el.configPathMirror.textContent = data.monitor_path; + el.imageCountMirror.textContent = `${state.activeImages.length} selected`; updateDirtyUi(); render(); } @@ -685,7 +787,7 @@ const INDEX_HTML: &str = r##" ${safeName}
${safeLabel}
-
${bytes(image.size)}${image.animated ? ` • GIF • ${image.frame_count} Frames` : ""}
+
${bytes(image.size)}${gifFrameLabel(image)}
@@ -742,7 +844,7 @@ const INDEX_HTML: &str = r##"
${index + 1}
${safeLabel}
-
${isCurrent ? "Currently shown" : (image && image.animated ? `Animated • ${image.frame_count} Frames` : "")}
+
${isCurrent ? "Currently shown" : (image && image.animated ? `Animated • ${orderFrameLabel(image)}` : "")}
@@ -769,7 +871,11 @@ const INDEX_HTML: &str = r##" el.specsEnabled.addEventListener("change", markDirty); el.memesEnabled.addEventListener("change", markDirty); el.gifsEnabled.addEventListener("change", markDirty); + el.tabButtons.forEach((button) => { + button.addEventListener("click", () => setActiveTab(button.dataset.tabTarget)); + }); + setActiveTab("screen-control"); load({ force: true }).catch((err) => toast(err.message)); setInterval(() => load().catch(() => {}), 5000);