This commit is contained in:
@@ -45,7 +45,8 @@ pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
|||||||
label: manifest.label,
|
label: manifest.label,
|
||||||
size: meta.len(),
|
size: meta.len(),
|
||||||
animated: true,
|
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 {
|
} else {
|
||||||
out.push(ImageView {
|
out.push(ImageView {
|
||||||
@@ -55,6 +56,7 @@ pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
|||||||
size: meta.len(),
|
size: meta.len(),
|
||||||
animated: false,
|
animated: false,
|
||||||
frame_count: 1,
|
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() {
|
if frames.is_empty() {
|
||||||
anyhow::bail!("GIF contains no frames");
|
anyhow::bail!("GIF contains no frames");
|
||||||
}
|
}
|
||||||
|
let source_frame_count = frames.len();
|
||||||
|
|
||||||
let base = make_image_stem(file_name);
|
let base = make_image_stem(file_name);
|
||||||
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
|
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()
|
.first()
|
||||||
.map(|frame| frame.name.clone())
|
.map(|frame| frame.name.clone())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
source_frame_count: Some(source_frame_count),
|
||||||
frames: stored_frames,
|
frames: stored_frames,
|
||||||
};
|
};
|
||||||
let manifest_path = state.image_dir.join(&manifest_name);
|
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<Rg
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use image::{Rgb, RgbImage};
|
use image::{Rgb, RgbImage};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::types::GifSetManifest;
|
||||||
|
|
||||||
use super::{collapse_rendered_frames, MIN_GIF_FRAME_DELAY_MS};
|
use super::{collapse_rendered_frames, MIN_GIF_FRAME_DELAY_MS};
|
||||||
|
|
||||||
@@ -286,4 +293,22 @@ mod tests {
|
|||||||
fn min_gif_frame_delay_supports_50ms_frames() {
|
fn min_gif_frame_delay_supports_50ms_frames() {
|
||||||
assert!(MIN_GIF_FRAME_DELAY_MS <= 50);
|
assert!(MIN_GIF_FRAME_DELAY_MS <= 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gif_manifest_without_source_frame_count_stays_compatible() {
|
||||||
|
let manifest: GifSetManifest = serde_json::from_value(json!({
|
||||||
|
"kind": "gif_set",
|
||||||
|
"version": 1,
|
||||||
|
"label": "demo.gif",
|
||||||
|
"preview": ".gifframe-000.jpg",
|
||||||
|
"frames": [
|
||||||
|
{ "name": ".gifframe-000.jpg", "delay_ms": 50 },
|
||||||
|
{ "name": ".gifframe-001.jpg", "delay_ms": 50 }
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(manifest.source_frame_count, None);
|
||||||
|
assert_eq!(manifest.frames.len(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ pub(crate) struct GifSetManifest {
|
|||||||
pub(crate) version: u32,
|
pub(crate) version: u32,
|
||||||
pub(crate) label: String,
|
pub(crate) label: String,
|
||||||
pub(crate) preview: String,
|
pub(crate) preview: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) source_frame_count: Option<usize>,
|
||||||
pub(crate) frames: Vec<GifFrameMeta>,
|
pub(crate) frames: Vec<GifFrameMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +119,7 @@ pub(crate) struct ImageView {
|
|||||||
pub(crate) url: String,
|
pub(crate) url: String,
|
||||||
pub(crate) animated: bool,
|
pub(crate) animated: bool,
|
||||||
pub(crate) frame_count: usize,
|
pub(crate) frame_count: usize,
|
||||||
|
pub(crate) display_frame_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
|||||||
+160
-54
@@ -87,6 +87,35 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
.brand p { margin: 3px 0 0; color: var(--muted); font-size: .9rem; }
|
.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; }
|
.layout { display: grid; grid-template-columns: minmax(0, 1.45fr) 520px; gap: 14px; }
|
||||||
.stack { display: grid; gap: 16px; }
|
.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 {
|
.panel, .card {
|
||||||
background: linear-gradient(180deg, rgba(18, 25, 34, .98), rgba(13, 18, 25, .98));
|
background: linear-gradient(180deg, rgba(18, 25, 34, .98), rgba(13, 18, 25, .98));
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -238,6 +267,15 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
grid-template-columns: minmax(0, 1.35fr) minmax(340px, .75fr);
|
grid-template-columns: minmax(0, 1.35fr) minmax(340px, .75fr);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
.system-tab {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.specs-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
.gallery {
|
.gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
||||||
@@ -352,7 +390,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
@media (max-width: 1180px) {
|
@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)); }
|
.status-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
}
|
}
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
@@ -388,33 +426,81 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="layout">
|
<nav class="tabbar" aria-label="Views">
|
||||||
<div class="stack">
|
<button class="tab-button active" type="button" data-tab-target="screen-control">Screen Control</button>
|
||||||
<div class="panel">
|
<button class="tab-button" type="button" data-tab-target="system-specs">System Specs</button>
|
||||||
<div class="section-head">
|
</nav>
|
||||||
<h2>Rotation Setup</h2>
|
|
||||||
<span class="subtle">/config/Monitor3.json</span>
|
<section class="tab-panel" data-tab-panel="screen-control">
|
||||||
</div>
|
<section class="layout">
|
||||||
<div class="controls">
|
<div class="stack">
|
||||||
<label>
|
<div class="panel">
|
||||||
<span>Switch Time</span>
|
<div class="section-head">
|
||||||
<input id="switchTime" type="number" min="1" max="600" value="10">
|
<h2>Rotation Setup</h2>
|
||||||
</label>
|
<span class="subtle">/config/Monitor3.json</span>
|
||||||
<label>
|
</div>
|
||||||
<span>Upload Source</span>
|
<div class="controls">
|
||||||
<input id="upload" type="file" accept="image/*">
|
<label>
|
||||||
</label>
|
<span>Switch Time</span>
|
||||||
<div class="actions">
|
<input id="switchTime" type="number" min="1" max="600" value="10">
|
||||||
<button id="disable" class="danger" type="button">Disable</button>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Upload Source</span>
|
||||||
|
<input id="upload" type="file" accept="image/*">
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="disable" class="danger" type="button">Disable</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
</div>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside class="status-grid">
|
||||||
|
<div class="card status"><span>Panel</span><strong id="customPanelValue">-</strong></div>
|
||||||
|
<div class="card status"><span>Images</span><strong id="imageCountValue">-</strong></div>
|
||||||
|
<div class="card status"><span>Display</span><strong id="displayStatusValue">-</strong></div>
|
||||||
|
<div class="card status"><span>Frame</span><strong id="currentImageValue">-</strong></div>
|
||||||
|
<div class="card status"><span>Target</span><strong id="displayTargetValue">-</strong></div>
|
||||||
|
<div class="card status"><span>Config</span><strong id="configPathValue">-</strong></div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workbench">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2>Available Images</h2>
|
||||||
|
<span class="subtle">960 x 376</span>
|
||||||
|
</div>
|
||||||
|
<div id="gallery" class="gallery"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2>Rotation Order</h2>
|
||||||
|
<span class="subtle">Live queue</span>
|
||||||
|
</div>
|
||||||
|
<div id="order" class="order"></div>
|
||||||
|
<p id="displayErrorValue" class="muted"></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab-panel system-tab" data-tab-panel="system-specs" hidden>
|
||||||
|
<section class="specs-summary">
|
||||||
|
<div class="card status"><span>Display</span><strong id="displayStatusMirror">-</strong><small id="displayTargetMirror">-</small></div>
|
||||||
|
<div class="card status"><span>Panel State</span><strong id="customPanelMirror">-</strong><small id="currentImageMirror">-</small></div>
|
||||||
|
<div class="card status"><span>Config</span><strong id="configPathMirror">-</strong><small id="imageCountMirror">-</small></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2>System Specs</h2>
|
||||||
|
<span class="subtle">Live host metrics</span>
|
||||||
|
</div>
|
||||||
<section class="metrics-grid">
|
<section class="metrics-grid">
|
||||||
<div class="card metric-card"><span class="metric-label">CPU</span><strong id="cpuUsageValue">-</strong><small id="cpuMetaValue">-</small></div>
|
<div class="card metric-card"><span class="metric-label">CPU</span><strong id="cpuUsageValue">-</strong><small id="cpuMetaValue">-</small></div>
|
||||||
<div class="card metric-card"><span class="metric-label">Memory</span><strong id="memUsageValue">-</strong><small id="memMetaValue">-</small></div>
|
<div class="card metric-card"><span class="metric-label">Memory</span><strong id="memUsageValue">-</strong><small id="memMetaValue">-</small></div>
|
||||||
@@ -423,34 +509,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<div class="card metric-card"><span class="metric-label">Load Avg</span><strong id="loadValue">-</strong><small id="uptimeValue">-</small></div>
|
<div class="card metric-card"><span class="metric-label">Load Avg</span><strong id="loadValue">-</strong><small id="uptimeValue">-</small></div>
|
||||||
<div class="card metric-card"><span class="metric-label">Temperatures</span><strong id="tempCpuValue">-</strong><small id="tempGpuValue">-</small></div>
|
<div class="card metric-card"><span class="metric-label">Temperatures</span><strong id="tempCpuValue">-</strong><small id="tempGpuValue">-</small></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<aside class="status-grid">
|
|
||||||
<div class="card status"><span>Panel</span><strong id="customPanelValue">-</strong></div>
|
|
||||||
<div class="card status"><span>Images</span><strong id="imageCountValue">-</strong></div>
|
|
||||||
<div class="card status"><span>Display</span><strong id="displayStatusValue">-</strong></div>
|
|
||||||
<div class="card status"><span>Frame</span><strong id="currentImageValue">-</strong></div>
|
|
||||||
<div class="card status"><span>Target</span><strong id="displayTargetValue">-</strong></div>
|
|
||||||
<div class="card status"><span>Config</span><strong id="configPathValue">-</strong></div>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="workbench">
|
|
||||||
<div class="panel">
|
|
||||||
<div class="section-head">
|
|
||||||
<h2>Available Images</h2>
|
|
||||||
<span class="subtle">960 x 376</span>
|
|
||||||
</div>
|
|
||||||
<div id="gallery" class="gallery"></div>
|
|
||||||
</div>
|
|
||||||
<div class="panel">
|
|
||||||
<div class="section-head">
|
|
||||||
<h2>Rotation Order</h2>
|
|
||||||
<span class="subtle">Live queue</span>
|
|
||||||
</div>
|
|
||||||
<div id="order" class="order"></div>
|
|
||||||
<p id="displayErrorValue" class="muted"></p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<div id="toast" class="toast"></div>
|
<div id="toast" class="toast"></div>
|
||||||
@@ -465,11 +524,14 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, memes_enabled: false, gifs_enabled: true },
|
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, memes_enabled: false, gifs_enabled: true },
|
||||||
display: {},
|
display: {},
|
||||||
system: {},
|
system: {},
|
||||||
|
activeTab: "screen-control",
|
||||||
dirty: false,
|
dirty: false,
|
||||||
};
|
};
|
||||||
const el = {
|
const el = {
|
||||||
gallery: document.getElementById("gallery"),
|
gallery: document.getElementById("gallery"),
|
||||||
order: document.getElementById("order"),
|
order: document.getElementById("order"),
|
||||||
|
tabButtons: Array.from(document.querySelectorAll("[data-tab-target]")),
|
||||||
|
tabPanels: Array.from(document.querySelectorAll("[data-tab-panel]")),
|
||||||
switchTime: document.getElementById("switchTime"),
|
switchTime: document.getElementById("switchTime"),
|
||||||
specsEnabled: document.getElementById("specsEnabled"),
|
specsEnabled: document.getElementById("specsEnabled"),
|
||||||
memesEnabled: document.getElementById("memesEnabled"),
|
memesEnabled: document.getElementById("memesEnabled"),
|
||||||
@@ -481,6 +543,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
displayTargetValue: document.getElementById("displayTargetValue"),
|
displayTargetValue: document.getElementById("displayTargetValue"),
|
||||||
displayErrorValue: document.getElementById("displayErrorValue"),
|
displayErrorValue: document.getElementById("displayErrorValue"),
|
||||||
configPathValue: document.getElementById("configPathValue"),
|
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"),
|
cpuUsageValue: document.getElementById("cpuUsageValue"),
|
||||||
cpuMetaValue: document.getElementById("cpuMetaValue"),
|
cpuMetaValue: document.getElementById("cpuMetaValue"),
|
||||||
memUsageValue: document.getElementById("memUsageValue"),
|
memUsageValue: document.getElementById("memUsageValue"),
|
||||||
@@ -522,6 +590,34 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
.replaceAll("'", "'");
|
.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) {
|
function move(index, delta) {
|
||||||
const target = index + delta;
|
const target = index + delta;
|
||||||
if (target < 0 || target >= state.activeImages.length) return;
|
if (target < 0 || target >= state.activeImages.length) return;
|
||||||
@@ -620,6 +716,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.uptimeValue.textContent = `Uptime ${state.system.uptime || "-"}`;
|
el.uptimeValue.textContent = `Uptime ${state.system.uptime || "-"}`;
|
||||||
el.tempCpuValue.textContent = `CPU ${state.system.temperature_cpu || "-"}`;
|
el.tempCpuValue.textContent = `CPU ${state.system.temperature_cpu || "-"}`;
|
||||||
el.tempGpuValue.textContent = `GPU ${state.system.temperature_gpu || "-"}`;
|
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();
|
updateDirtyUi();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -685,7 +787,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<img src="${safeUrl}" alt="${safeName}">
|
<img src="${safeUrl}" alt="${safeName}">
|
||||||
<div class="tile-body">
|
<div class="tile-body">
|
||||||
<div class="tile-title">${safeLabel}</div>
|
<div class="tile-title">${safeLabel}</div>
|
||||||
<div class="tile-meta">${bytes(image.size)}${image.animated ? ` • GIF • ${image.frame_count} Frames` : ""}</div>
|
<div class="tile-meta">${bytes(image.size)}${gifFrameLabel(image)}</div>
|
||||||
<div class="mini">
|
<div class="mini">
|
||||||
<button class="ghost" type="button" title="${selected ? "Remove" : "Add"}">${selected ? "Remove" : "Add"}</button>
|
<button class="ghost" type="button" title="${selected ? "Remove" : "Add"}">${selected ? "Remove" : "Add"}</button>
|
||||||
<button class="danger" type="button">Delete</button>
|
<button class="danger" type="button">Delete</button>
|
||||||
@@ -742,7 +844,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<div class="index">${index + 1}</div>
|
<div class="index">${index + 1}</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div>${safeLabel}</div>
|
<div>${safeLabel}</div>
|
||||||
<div class="muted">${isCurrent ? "Currently shown" : (image && image.animated ? `Animated • ${image.frame_count} Frames` : "")}</div>
|
<div class="muted">${isCurrent ? "Currently shown" : (image && image.animated ? `Animated • ${orderFrameLabel(image)}` : "")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini">
|
<div class="mini">
|
||||||
<button class="ghost" type="button" title="Move up" aria-label="Move ${safeLabel} up">↑</button>
|
<button class="ghost" type="button" title="Move up" aria-label="Move ${safeLabel} up">↑</button>
|
||||||
@@ -769,7 +871,11 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.specsEnabled.addEventListener("change", markDirty);
|
el.specsEnabled.addEventListener("change", markDirty);
|
||||||
el.memesEnabled.addEventListener("change", markDirty);
|
el.memesEnabled.addEventListener("change", markDirty);
|
||||||
el.gifsEnabled.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));
|
load({ force: true }).catch((err) => toast(err.message));
|
||||||
setInterval(() => load().catch(() => {}), 5000);
|
setInterval(() => load().catch(() => {}), 5000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user