Add display toggles for specs and memes
Rust / Clippy, Rustfmt, Tests (push) Waiting to run
Rust / Linux-x64 build (push) Blocked by required conditions
Rust / GitHub release (push) Blocked by required conditions

This commit is contained in:
2026-06-09 22:37:57 +02:00
parent 0304f1428f
commit f9bfd7639c
3 changed files with 399 additions and 48 deletions
Generated
+3
View File
@@ -156,6 +156,7 @@ dependencies = [
name = "aster-webui"
version = "0.1.0"
dependencies = [
"ab_glyph",
"anyhow",
"asterctl",
"asterctl-lcd",
@@ -163,6 +164,7 @@ dependencies = [
"chrono",
"clap",
"image",
"imageproc",
"mime_guess",
"serde",
"serde_json",
@@ -894,6 +896,7 @@ dependencies = [
"num",
"rand",
"rand_distr",
"rayon",
]
[[package]]
+2
View File
@@ -10,6 +10,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
ab_glyph = { version = "0.2.31", default-features = false, features = ["std"] }
anyhow = "1.0.98"
asterctl = { path = "../asterctl", version = "0.2.0" }
asterctl-lcd = { path = "../asterctl-lcd", version = "0.2.0" }
@@ -17,6 +18,7 @@ axum = { version = "0.8.4", features = ["multipart"] }
chrono = "0.4"
clap = { version = "4.5.42", features = ["derive"] }
image = "0.25.6"
imageproc = "0.25.0"
mime_guess = "2.0.5"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
+394 -48
View File
@@ -1,11 +1,12 @@
use std::{
net::SocketAddr,
path::PathBuf,
sync::{Arc, RwLock},
sync::{Arc, OnceLock, RwLock},
thread,
time::{Duration, Instant},
};
use ab_glyph::{FontArc, PxScale};
use anyhow::{Context, Result};
use asterctl::img;
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
@@ -19,7 +20,11 @@ use axum::{
};
use chrono::{Local, Utc};
use clap::Parser;
use image::{DynamicImage, ImageFormat, RgbImage, imageops::FilterType};
use image::{DynamicImage, ImageFormat, Rgb, RgbImage, imageops::FilterType};
use imageproc::{
drawing::{draw_filled_rect_mut, draw_hollow_rect_mut, draw_text_mut},
rect::Rect,
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sysinfo::{Components, Disks, System};
@@ -28,6 +33,7 @@ use tracing::{error, info, warn};
const DISPLAY_WIDTH: u32 = 960;
const DISPLAY_HEIGHT: u32 = 376;
const SYSTEM_FRAME_NAME: &str = "System Specs";
#[derive(Parser, Debug)]
#[command(author, version, about)]
@@ -68,13 +74,32 @@ struct DisplayConfig {
struct RotationSnapshot {
custom_panel: bool,
switch_time: u32,
specs_enabled: bool,
memes_enabled: bool,
active_images: Vec<String>,
}
impl RotationSnapshot {
fn rotation_active(&self) -> bool {
self.custom_panel && !self.active_images.is_empty()
self.custom_panel && !self.frames().is_empty()
}
fn frames(&self) -> Vec<RotationFrame> {
let mut frames = Vec::new();
if self.memes_enabled {
frames.extend(self.active_images.iter().cloned().map(RotationFrame::Image));
}
if frames.is_empty() && self.specs_enabled {
frames.push(RotationFrame::SystemSpecs);
}
frames
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum RotationFrame {
SystemSpecs,
Image(String),
}
#[derive(Clone, Serialize)]
@@ -92,6 +117,8 @@ struct StateResponse {
struct SetupView {
custom_panel: bool,
switch_time: String,
specs_enabled: bool,
memes_enabled: bool,
}
#[derive(Clone, Serialize)]
@@ -108,6 +135,8 @@ struct DisplayStatus {
mode: String,
device: String,
custom_panel: bool,
specs_enabled: bool,
memes_enabled: bool,
rotation_active: bool,
switch_time: String,
active_images: Vec<String>,
@@ -142,6 +171,8 @@ struct SystemView {
struct ActivateRequest {
images: Vec<String>,
switch_time: Option<u32>,
specs_enabled: Option<bool>,
memes_enabled: Option<bool>,
}
#[derive(Deserialize)]
@@ -211,6 +242,8 @@ fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
mode: display_mode(config),
device: display_target(config),
custom_panel: false,
specs_enabled: false,
memes_enabled: false,
rotation_active: false,
switch_time: "10".into(),
active_images: Vec::new(),
@@ -246,6 +279,8 @@ async fn ensure_layout(state: &AppState) -> Result<()> {
"customPanel": false,
"language": 1,
"switchTime": "10",
"nativeSpecs": false,
"nativeMemes": true,
"operationMode": 0,
"theme": 1,
"diskUpdate": 300,
@@ -350,6 +385,7 @@ async fn api_activate(
Json(payload): Json<ActivateRequest>,
) -> Response {
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
let specs_enabled = payload.specs_enabled.unwrap_or(false);
let available = match list_images(&state).await {
Ok(items) => items,
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
@@ -363,8 +399,15 @@ async fn api_activate(
}
}
if valid.is_empty() {
return error_response(StatusCode::BAD_REQUEST, "No valid images selected");
let memes_enabled = payload
.memes_enabled
.unwrap_or(!valid.is_empty());
if !specs_enabled && (!memes_enabled || valid.is_empty()) {
return error_response(
StatusCode::BAD_REQUEST,
"Enable specs or select at least one meme image",
);
}
let mut monitor = match load_monitor_json(&state).await {
@@ -372,7 +415,7 @@ async fn api_activate(
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
};
set_custom_panels(&mut monitor, true, switch_time, &valid);
set_custom_panels(&mut monitor, switch_time, specs_enabled, memes_enabled, &valid);
if let Err(err) = save_monitor_json(&state, &monitor).await {
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
@@ -381,6 +424,8 @@ async fn api_activate(
Json(json!({
"ok": true,
"switchTime": switch_time,
"specsEnabled": specs_enabled,
"memesEnabled": memes_enabled,
"activeImages": valid,
}))
.into_response()
@@ -391,10 +436,10 @@ async fn api_disable(State(state): State<Arc<AppState>>) -> Response {
Ok(value) => value,
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
};
let switch_time = current_switch_time(&monitor);
let active_images = current_active_images(&monitor);
if let Some(setup) = monitor.get_mut("setup").and_then(Value::as_object_mut) {
setup.insert("customPanel".into(), Value::Bool(false));
}
set_custom_panels(&mut monitor, switch_time, false, false, &active_images);
if let Err(err) = save_monitor_json(&state, &monitor).await {
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
@@ -427,15 +472,12 @@ async fn api_delete(
let mut active_images = current_active_images(&monitor);
let switch_time = current_switch_time(&monitor);
let specs_enabled = current_specs_enabled(&monitor);
let memes_enabled = current_memes_enabled(&monitor);
let before = active_images.len();
active_images.retain(|name| name != &payload.name);
if active_images.len() != before {
set_custom_panels(
&mut monitor,
!active_images.is_empty(),
switch_time,
&active_images,
);
set_custom_panels(&mut monitor, switch_time, specs_enabled, memes_enabled, &active_images);
if let Err(err) = save_monitor_json(&state, &monitor).await {
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
}
@@ -488,6 +530,8 @@ async fn build_state_response(state: &AppState) -> Result<StateResponse> {
.and_then(Value::as_str)
.unwrap_or("10")
.to_string();
let specs_enabled = current_specs_enabled(&monitor);
let memes_enabled = current_memes_enabled(&monitor);
Ok(StateResponse {
monitor_path: state.monitor_path.display().to_string(),
@@ -495,6 +539,8 @@ async fn build_state_response(state: &AppState) -> Result<StateResponse> {
setup: SetupView {
custom_panel,
switch_time,
specs_enabled,
memes_enabled,
},
active_images: current_active_images(&monitor),
images: list_images(state).await?,
@@ -653,15 +699,42 @@ fn current_custom_panel_enabled(monitor: &Value) -> bool {
.unwrap_or(false)
}
fn current_specs_enabled(monitor: &Value) -> bool {
monitor
.get("setup")
.and_then(Value::as_object)
.and_then(|setup| setup.get("nativeSpecs"))
.and_then(Value::as_bool)
.unwrap_or(false)
}
fn current_memes_enabled(monitor: &Value) -> bool {
monitor
.get("setup")
.and_then(Value::as_object)
.and_then(|setup| setup.get("nativeMemes"))
.and_then(Value::as_bool)
.unwrap_or_else(|| !current_active_images(monitor).is_empty())
}
fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
RotationSnapshot {
custom_panel: current_custom_panel_enabled(monitor),
switch_time: current_switch_time(monitor),
specs_enabled: current_specs_enabled(monitor),
memes_enabled: current_memes_enabled(monitor),
active_images: current_active_images(monitor),
}
}
fn set_custom_panels(monitor: &mut Value, enabled: bool, switch_time: u32, images: &[String]) {
fn set_custom_panels(
monitor: &mut Value,
switch_time: u32,
specs_enabled: bool,
memes_enabled: bool,
images: &[String],
) {
let enabled = specs_enabled || (memes_enabled && !images.is_empty());
let setup = monitor
.as_object_mut()
.expect("monitor config must be an object")
@@ -671,6 +744,8 @@ fn set_custom_panels(monitor: &mut Value, enabled: bool, switch_time: u32, image
if let Some(setup) = setup.as_object_mut() {
setup.insert("customPanel".into(), Value::Bool(enabled));
setup.insert("switchTime".into(), Value::String(switch_time.to_string()));
setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled));
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
}
monitor["mianban"] = Value::Array(
@@ -714,6 +789,200 @@ fn render_display_panel(image: &DynamicImage) -> RgbImage {
canvas
}
fn render_system_panel() -> RgbImage {
let system = collect_system_view();
let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, Rgb([10, 14, 18]));
let host_name = System::host_name().unwrap_or_else(|| "Unraid".into());
let timestamp = Local::now().format("%d.%m.%Y %H:%M:%S").to_string();
draw_filled_rect_mut(
&mut canvas,
Rect::at(0, 0).of_size(DISPLAY_WIDTH, 84),
Rgb([18, 27, 36]),
);
draw_filled_rect_mut(
&mut canvas,
Rect::at(0, 84).of_size(DISPLAY_WIDTH, DISPLAY_HEIGHT - 84),
Rgb([8, 11, 15]),
);
draw_text_line(
&mut canvas,
Rgb([216, 253, 114]),
28,
20,
36.0,
"AOOSTAR Native Specs",
);
draw_text_line(
&mut canvas,
Rgb([146, 163, 181]),
30,
58,
20.0,
&format!("{host_name} | {timestamp}"),
);
let cards = [
(
24,
106,
"CPU",
format!("{} %", system.cpu_usage_percent),
format!(
"Load {} / {} / {}",
system.load_avg_one, system.load_avg_five, system.load_avg_fifteen
),
Rgb([124, 231, 191]),
),
(
332,
106,
"Memory",
format!("{} %", system.mem_usage_percent),
format!("{} / {}", system.mem_used, system.mem_total),
Rgb([125, 196, 255]),
),
(
640,
106,
"Storage",
format!("{} %", system.disk_usage_percent),
format!("{} / {}", system.disk_used, system.disk_total),
Rgb([255, 199, 115]),
),
(
24,
236,
"Swap",
format!("{} %", system.swap_usage_percent),
format!("{} / {}", system.swap_used, system.swap_total),
Rgb([194, 167, 255]),
),
(
332,
236,
"Temperatures",
format!("CPU {}", system.temperature_cpu.as_deref().unwrap_or("-")),
format!("GPU {}", system.temperature_gpu.as_deref().unwrap_or("-")),
Rgb([255, 127, 115]),
),
(
640,
236,
"System",
system.uptime.clone(),
format!("{} CPU / {} proc", system.cpu_count, system.process_count),
Rgb([216, 253, 114]),
),
];
for (x, y, title, value, meta, accent) in cards {
draw_metric_card(&mut canvas, x, y, title, &value, &meta, accent);
}
canvas
}
fn overlay_system_specs(canvas: &mut RgbImage) {
let system = collect_system_view();
let host_name = System::host_name().unwrap_or_else(|| "Unraid".into());
let timestamp = Local::now().format("%H:%M:%S").to_string();
draw_filled_rect_mut(canvas, Rect::at(18, 16).of_size(924, 54), Rgb([12, 18, 24]));
draw_hollow_rect_mut(canvas, Rect::at(18, 16).of_size(924, 54), Rgb([36, 50, 67]));
draw_text_line(
canvas,
Rgb([216, 253, 114]),
34,
28,
22.0,
&format!("{host_name} CPU {}% RAM {}% Disk {}%", system.cpu_usage_percent, system.mem_usage_percent, system.disk_usage_percent),
);
draw_text_line(
canvas,
Rgb([146, 163, 181]),
34,
52,
16.0,
&format!(
"Load {} / {} / {} Temp {} / {} Uptime {} {}",
system.load_avg_one,
system.load_avg_five,
system.load_avg_fifteen,
system.temperature_cpu.as_deref().unwrap_or("-"),
system.temperature_gpu.as_deref().unwrap_or("-"),
system.uptime,
timestamp
),
);
draw_filled_rect_mut(canvas, Rect::at(18, 306).of_size(924, 52), Rgb([12, 18, 24]));
draw_hollow_rect_mut(canvas, Rect::at(18, 306).of_size(924, 52), Rgb([36, 50, 67]));
draw_text_line(
canvas,
Rgb([237, 242, 247]),
34,
320,
18.0,
&format!(
"Mem {} / {} Swap {} / {} Proc {} CPU {} GPU {}",
system.mem_used,
system.mem_total,
system.swap_used,
system.swap_total,
system.process_count,
system.temperature_cpu.as_deref().unwrap_or("-"),
system.temperature_gpu.as_deref().unwrap_or("-"),
),
);
}
fn draw_metric_card(
canvas: &mut RgbImage,
x: i32,
y: i32,
title: &str,
value: &str,
meta: &str,
accent: Rgb<u8>,
) {
draw_filled_rect_mut(canvas, Rect::at(x, y).of_size(284, 112), Rgb([16, 22, 30]));
draw_hollow_rect_mut(canvas, Rect::at(x, y).of_size(284, 112), Rgb([36, 50, 67]));
draw_filled_rect_mut(canvas, Rect::at(x + 1, y + 1).of_size(6, 110), accent);
draw_text_line(canvas, accent, x + 20, y + 16, 19.0, title);
draw_text_line(canvas, Rgb([237, 242, 247]), x + 20, y + 44, 28.0, value);
draw_text_line(canvas, Rgb([146, 163, 181]), x + 20, y + 82, 18.0, meta);
}
fn draw_text_line(
canvas: &mut RgbImage,
color: Rgb<u8>,
x: i32,
y: i32,
size: f32,
text: &str,
) {
draw_text_mut(
canvas,
color,
x,
y,
PxScale { x: size, y: size },
display_font(),
text,
);
}
fn display_font() -> &'static FontArc {
static FONT: OnceLock<FontArc> = OnceLock::new();
FONT.get_or_init(|| {
FontArc::try_from_slice(include_bytes!("../../../fonts/DejaVuSans.ttf"))
.expect("embedded display font must be valid")
})
}
fn make_image_name(file_name: &str) -> String {
let base = file_name
.rsplit_once('.')
@@ -803,9 +1072,8 @@ fn open_screen(config: &DisplayConfig) -> Result<AooScreen> {
fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
let mut snapshot = RotationSnapshot::default();
let mut next_index = 0usize;
let mut last_switch_at = Instant::now();
let mut force_send = true;
let mut cycle_started_at = Instant::now();
let mut current_frame_index = None;
loop {
let monitor = match load_monitor_json_sync(&state.monitor_path) {
@@ -824,12 +1092,16 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
let new_snapshot = rotation_snapshot(&monitor);
if new_snapshot != snapshot {
snapshot = new_snapshot;
next_index = 0;
force_send = true;
cycle_started_at = Instant::now();
current_frame_index = None;
}
let frames = snapshot.frames();
update_display_status(&state.display_status, |status| {
status.custom_panel = snapshot.custom_panel;
status.specs_enabled = snapshot.specs_enabled;
status.memes_enabled = snapshot.memes_enabled;
status.rotation_active = snapshot.rotation_active();
status.switch_time = snapshot.switch_time.to_string();
status.active_images = snapshot.active_images.clone();
@@ -846,41 +1118,61 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
}
let switch_after = Duration::from_secs(snapshot.switch_time as u64);
let send_due = force_send || last_switch_at.elapsed() >= switch_after;
let elapsed = cycle_started_at.elapsed();
let frame_slot = elapsed.as_secs() / snapshot.switch_time as u64;
let next_index = (frame_slot as usize) % frames.len();
let send_due = current_frame_index != Some(next_index);
if send_due {
let image_name = snapshot.active_images[next_index].clone();
match load_panel_rgb(&state.image_dir, &image_name) {
Ok(rgb_img) => {
let frame = frames[next_index].clone();
match frame {
RotationFrame::SystemSpecs => {
let panel_name = SYSTEM_FRAME_NAME.to_string();
let rgb_img = render_system_panel();
screen
.send_image(&rgb_img)
.with_context(|| format!("Failed to send panel {image_name}"))?;
.context("Failed to send system specs panel")?;
update_display_status(&state.display_status, |status| {
status.current_image = Some(image_name.clone());
status.current_image = Some(panel_name);
status.last_error = None;
status.connected = true;
status.updated_at = Some(stamp_now());
});
}
Err(err) => {
warn!("Skipping panel {image_name}: {err}");
update_display_status(&state.display_status, |status| {
status.current_image = Some(image_name.clone());
status.last_error =
Some(format!("Panel {image_name} could not be rendered: {err}"));
status.connected = true;
status.updated_at = Some(stamp_now());
});
}
}
RotationFrame::Image(image_name) => match load_panel_rgb(&state.image_dir, &image_name)
{
Ok(mut rgb_img) => {
if snapshot.specs_enabled {
overlay_system_specs(&mut rgb_img);
}
screen
.send_image(&rgb_img)
.with_context(|| format!("Failed to send panel {image_name}"))?;
last_switch_at = Instant::now();
force_send = false;
next_index = (next_index + 1) % snapshot.active_images.len();
update_display_status(&state.display_status, |status| {
status.current_image = Some(image_name.clone());
status.last_error = None;
status.connected = true;
status.updated_at = Some(stamp_now());
});
}
Err(err) => {
warn!("Skipping panel {image_name}: {err}");
update_display_status(&state.display_status, |status| {
status.current_image = Some(image_name.clone());
status.last_error =
Some(format!("Panel {image_name} could not be rendered: {err}"));
status.connected = true;
status.updated_at = Some(stamp_now());
});
}
},
}
current_frame_index = Some(next_index);
}
thread::sleep(rotation_sleep(snapshot.active_images.len(), switch_after, last_switch_at));
thread::sleep(rotation_sleep(frames.len(), switch_after, cycle_started_at));
}
}
@@ -897,12 +1189,15 @@ fn load_panel_rgb(image_dir: &PathBuf, image_name: &str) -> Result<RgbImage> {
Ok(image.to_rgb8())
}
fn rotation_sleep(image_count: usize, switch_after: Duration, last_switch_at: Instant) -> Duration {
if image_count <= 1 {
fn rotation_sleep(frame_count: usize, switch_after: Duration, cycle_started_at: Instant) -> Duration {
if frame_count <= 1 {
return Duration::from_millis(750);
}
let remaining = switch_after.saturating_sub(last_switch_at.elapsed());
let elapsed = cycle_started_at.elapsed();
let switch_secs = switch_after.as_secs().max(1);
let next_boundary_secs = ((elapsed.as_secs() / switch_secs) + 1) * switch_secs;
let remaining = Duration::from_secs(next_boundary_secs).saturating_sub(elapsed);
if remaining > Duration::from_millis(750) {
Duration::from_millis(750)
} else if remaining < Duration::from_millis(200) {
@@ -1012,6 +1307,13 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
.status strong { display: block; margin-top: 8px; font-size: 1.3rem; }
.status small { display: block; margin-top: 6px; color: var(--muted); }
.controls { display: grid; grid-template-columns: 180px 1fr auto; gap: 12px; align-items: end; margin-top: 16px; }
.toggle-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.toggle-pill {
display: inline-flex; align-items: center; gap: 10px; padding: 10px 14px;
border: 1px solid var(--line); border-radius: 999px; background: rgba(9,14,20,.7);
color: var(--text); font-size: .95rem;
}
.toggle-pill input { margin: 0; width: 18px; height: 18px; accent-color: var(--accent); }
label { display: grid; gap: 8px; color: var(--muted); font-size: .9rem; }
input, button {
border-radius: 14px;
@@ -1078,6 +1380,10 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
<button id="refresh" class="ghost" type="button">Refresh</button>
</div>
</div>
<div class="toggle-row">
<label class="toggle-pill"><input id="specsEnabled" type="checkbox"> System Specs on LCD</label>
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes on LCD</label>
</div>
</div>
<div class="status-grid">
<div class="card status"><span class="muted">Custom Panel</span><strong id="customPanelValue">-</strong></div>
@@ -1113,7 +1419,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
const state = {
images: [],
activeImages: [],
setup: { switch_time: "10", custom_panel: false },
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, memes_enabled: false },
display: {},
system: {},
};
@@ -1121,6 +1427,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
gallery: document.getElementById("gallery"),
order: document.getElementById("order"),
switchTime: document.getElementById("switchTime"),
specsEnabled: document.getElementById("specsEnabled"),
memesEnabled: document.getElementById("memesEnabled"),
customPanelValue: document.getElementById("customPanelValue"),
imageCountValue: document.getElementById("imageCountValue"),
displayStatusValue: document.getElementById("displayStatusValue"),
@@ -1194,7 +1502,15 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
state.display = data.display || {};
state.system = data.system || {};
el.switchTime.value = data.setup.switch_time || "10";
el.customPanelValue.textContent = data.setup.custom_panel ? "Active" : "Disabled";
el.specsEnabled.checked = !!data.setup.specs_enabled;
el.memesEnabled.checked = !!data.setup.memes_enabled;
const enabledModes = [
data.setup.specs_enabled ? "Specs" : "",
data.setup.memes_enabled ? "Memes" : "",
].filter(Boolean);
el.customPanelValue.textContent = data.setup.custom_panel
? (enabledModes.join(" + ") || "Active")
: "Disabled";
el.imageCountValue.textContent = String(data.active_images.length);
el.displayStatusValue.textContent = state.display.connected
? `Connected (${state.display.mode || "native"})`
@@ -1236,6 +1552,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
body: JSON.stringify({
images: state.activeImages,
switch_time: Number.parseInt(el.switchTime.value || "10", 10),
specs_enabled: el.specsEnabled.checked,
memes_enabled: el.memesEnabled.checked,
}),
});
toast("Rotation updated");
@@ -1282,7 +1600,35 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
}
el.order.innerHTML = "";
if (!state.activeImages.length) {
if (state.setup.specs_enabled && !state.activeImages.length) {
const row = document.createElement("div");
row.className = "order-item";
row.innerHTML = `
<div class="index">S</div>
<div class="meta">
<div>System Specs</div>
<div class="muted">${state.display.current_image === "System Specs" ? "Currently shown" : "Fixed first frame when enabled"}</div>
</div>
<div class="mini"><span class="muted">Toggle above</span></div>
`;
el.order.appendChild(row);
}
if (state.setup.specs_enabled && state.activeImages.length) {
const row = document.createElement("div");
row.className = "order-item";
row.innerHTML = `
<div class="index">S</div>
<div class="meta">
<div>System Specs Overlay</div>
<div class="muted">Live stats are drawn over every meme while enabled.</div>
</div>
<div class="mini"><span class="muted">Toggle above</span></div>
`;
el.order.appendChild(row);
}
if (!state.activeImages.length && !state.setup.specs_enabled) {
el.order.innerHTML = `<p class="muted">No active images.</p>`;
return;
}