feat: refine system specs screen layout
Build And Push Container / build-and-push (push) Successful in 56s

This commit is contained in:
2026-06-30 23:35:45 +02:00
parent c5f0249dbf
commit 382e82601a
6 changed files with 307 additions and 19 deletions
+3 -1
View File
@@ -177,7 +177,7 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
if send_due { if send_due {
if active_frame.image_name == SYSTEM_FRAME_NAME { if active_frame.image_name == SYSTEM_FRAME_NAME {
let panel_name = SYSTEM_FRAME_NAME.to_string(); let panel_name = SYSTEM_FRAME_NAME.to_string();
let rgb_img = render_system_panel(); let rgb_img = render_system_panel(&snapshot.specs_mode);
prepare_screen_for_frame(screen, &active_frame); prepare_screen_for_frame(screen, &active_frame);
screen screen
.send_image(&rgb_img) .send_image(&rgb_img)
@@ -660,6 +660,7 @@ mod tests {
custom_panel: true, custom_panel: true,
switch_time: 7, switch_time: 7,
specs_enabled: false, specs_enabled: false,
specs_mode: "cards".into(),
memes_enabled: false, memes_enabled: false,
gifs_enabled: true, gifs_enabled: true,
active_images: vec![manifest_name.into()], active_images: vec![manifest_name.into()],
@@ -702,6 +703,7 @@ mod tests {
custom_panel: true, custom_panel: true,
switch_time: 7, switch_time: 7,
specs_enabled: false, specs_enabled: false,
specs_mode: "cards".into(),
memes_enabled: false, memes_enabled: false,
gifs_enabled: false, gifs_enabled: false,
active_images: vec![manifest_name.into()], active_images: vec![manifest_name.into()],
+26
View File
@@ -32,6 +32,7 @@ pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
"language": 1, "language": 1,
"switchTime": "10", "switchTime": "10",
"nativeSpecs": false, "nativeSpecs": false,
"nativeSpecsMode": "cards",
"nativeMemes": true, "nativeMemes": true,
"nativeGifs": true, "nativeGifs": true,
"operationMode": 0, "operationMode": 0,
@@ -93,6 +94,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
.unwrap_or("10") .unwrap_or("10")
.to_string(); .to_string();
let specs_enabled = current_specs_enabled(&monitor); let specs_enabled = current_specs_enabled(&monitor);
let specs_mode = current_specs_mode(&monitor);
let memes_enabled = current_memes_enabled(&monitor); let memes_enabled = current_memes_enabled(&monitor);
let gifs_enabled = current_gifs_enabled(&monitor); let gifs_enabled = current_gifs_enabled(&monitor);
@@ -103,6 +105,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
custom_panel, custom_panel,
switch_time, switch_time,
specs_enabled, specs_enabled,
specs_mode,
memes_enabled, memes_enabled,
gifs_enabled, gifs_enabled,
}, },
@@ -162,6 +165,16 @@ pub(crate) fn current_memes_enabled(monitor: &Value) -> bool {
.unwrap_or_else(|| !current_active_images(monitor).is_empty()) .unwrap_or_else(|| !current_active_images(monitor).is_empty())
} }
pub(crate) fn current_specs_mode(monitor: &Value) -> String {
monitor
.get("setup")
.and_then(Value::as_object)
.and_then(|setup| setup.get("nativeSpecsMode"))
.and_then(Value::as_str)
.map(normalize_specs_mode)
.unwrap_or_else(|| "cards".into())
}
pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool { pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool {
monitor monitor
.get("setup") .get("setup")
@@ -176,6 +189,7 @@ pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
custom_panel: current_custom_panel_enabled(monitor), custom_panel: current_custom_panel_enabled(monitor),
switch_time: current_switch_time(monitor), switch_time: current_switch_time(monitor),
specs_enabled: current_specs_enabled(monitor), specs_enabled: current_specs_enabled(monitor),
specs_mode: current_specs_mode(monitor),
memes_enabled: current_memes_enabled(monitor), memes_enabled: current_memes_enabled(monitor),
gifs_enabled: current_gifs_enabled(monitor), gifs_enabled: current_gifs_enabled(monitor),
active_images: current_active_images(monitor), active_images: current_active_images(monitor),
@@ -186,6 +200,7 @@ pub(crate) fn set_custom_panels(
monitor: &mut Value, monitor: &mut Value,
switch_time: u32, switch_time: u32,
specs_enabled: bool, specs_enabled: bool,
specs_mode: &str,
memes_enabled: bool, memes_enabled: bool,
gifs_enabled: bool, gifs_enabled: bool,
images: &[String], images: &[String],
@@ -201,6 +216,10 @@ pub(crate) fn set_custom_panels(
setup.insert("customPanel".into(), Value::Bool(enabled)); setup.insert("customPanel".into(), Value::Bool(enabled));
setup.insert("switchTime".into(), Value::String(switch_time.to_string())); setup.insert("switchTime".into(), Value::String(switch_time.to_string()));
setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled)); setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled));
setup.insert(
"nativeSpecsMode".into(),
Value::String(normalize_specs_mode(specs_mode)),
);
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled)); setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled)); setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled));
} }
@@ -218,6 +237,13 @@ pub(crate) fn set_custom_panels(
); );
} }
fn normalize_specs_mode(value: &str) -> String {
match value {
"meters" => "meters".into(),
_ => "cards".into(),
}
}
pub(crate) fn load_monitor_json_sync(path: &PathBuf) -> Result<Value> { pub(crate) fn load_monitor_json_sync(path: &PathBuf) -> Result<Value> {
let raw = std::fs::read_to_string(path) let raw = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?; .with_context(|| format!("Failed to read {}", path.display()))?;
+19 -2
View File
@@ -18,8 +18,8 @@ use crate::{
}, },
monitor::{ monitor::{
build_state_response, current_active_images, current_gifs_enabled, current_memes_enabled, build_state_response, current_active_images, current_gifs_enabled, current_memes_enabled,
current_specs_enabled, current_switch_time, load_monitor_json, save_monitor_json, current_specs_enabled, current_specs_mode, current_switch_time, load_monitor_json,
set_custom_panels, save_monitor_json, set_custom_panels,
}, },
types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse}, types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse},
ui::index_response, ui::index_response,
@@ -103,6 +103,17 @@ async fn api_activate(
) -> Response { ) -> Response {
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600); let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
let specs_enabled = payload.specs_enabled.unwrap_or(false); let specs_enabled = payload.specs_enabled.unwrap_or(false);
let specs_mode = payload
.specs_mode
.as_deref()
.map(|value| {
if value == "meters" {
"meters".to_string()
} else {
"cards".to_string()
}
})
.unwrap_or_else(|| "cards".to_string());
let available = match list_images(&state).await { let available = match list_images(&state).await {
Ok(items) => items, Ok(items) => items,
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
@@ -140,6 +151,7 @@ async fn api_activate(
&mut monitor, &mut monitor,
switch_time, switch_time,
specs_enabled, specs_enabled,
&specs_mode,
memes_enabled, memes_enabled,
gifs_enabled, gifs_enabled,
&valid, &valid,
@@ -153,6 +165,7 @@ async fn api_activate(
"ok": true, "ok": true,
"switchTime": switch_time, "switchTime": switch_time,
"specsEnabled": specs_enabled, "specsEnabled": specs_enabled,
"specsMode": specs_mode,
"memesEnabled": memes_enabled, "memesEnabled": memes_enabled,
"gifsEnabled": gifs_enabled, "gifsEnabled": gifs_enabled,
"activeImages": valid, "activeImages": valid,
@@ -167,11 +180,13 @@ async fn api_disable(State(state): State<Arc<AppState>>) -> Response {
}; };
let switch_time = current_switch_time(&monitor); let switch_time = current_switch_time(&monitor);
let active_images = current_active_images(&monitor); let active_images = current_active_images(&monitor);
let specs_mode = current_specs_mode(&monitor);
set_custom_panels( set_custom_panels(
&mut monitor, &mut monitor,
switch_time, switch_time,
false, false,
&specs_mode,
false, false,
false, false,
&active_images, &active_images,
@@ -209,6 +224,7 @@ async fn api_delete(
let mut active_images = current_active_images(&monitor); let mut active_images = current_active_images(&monitor);
let switch_time = current_switch_time(&monitor); let switch_time = current_switch_time(&monitor);
let specs_enabled = current_specs_enabled(&monitor); let specs_enabled = current_specs_enabled(&monitor);
let specs_mode = current_specs_mode(&monitor);
let memes_enabled = current_memes_enabled(&monitor); let memes_enabled = current_memes_enabled(&monitor);
let gifs_enabled = current_gifs_enabled(&monitor); let gifs_enabled = current_gifs_enabled(&monitor);
let before = active_images.len(); let before = active_images.len();
@@ -218,6 +234,7 @@ async fn api_delete(
&mut monitor, &mut monitor,
switch_time, switch_time,
specs_enabled, specs_enabled,
&specs_mode,
memes_enabled, memes_enabled,
gifs_enabled, gifs_enabled,
&active_images, &active_images,
+185 -2
View File
@@ -14,6 +14,13 @@ use crate::types::SystemView;
pub(crate) const DISPLAY_WIDTH: u32 = 960; pub(crate) const DISPLAY_WIDTH: u32 = 960;
pub(crate) const DISPLAY_HEIGHT: u32 = 376; pub(crate) const DISPLAY_HEIGHT: u32 = 376;
pub(crate) fn normalize_specs_mode(mode: &str) -> &'static str {
match mode {
"meters" => "meters",
_ => "cards",
}
}
pub(crate) fn collect_system_view() -> SystemView { pub(crate) fn collect_system_view() -> SystemView {
fn mount_usage(disks: &Disks, mount_point: &str) -> (String, String, String) { fn mount_usage(disks: &Disks, mount_point: &str) -> (String, String, String) {
disks disks
@@ -140,7 +147,14 @@ fn format_bytes(bytes: u64) -> String {
} }
} }
pub(crate) fn render_system_panel() -> RgbImage { pub(crate) fn render_system_panel(mode: &str) -> RgbImage {
match normalize_specs_mode(mode) {
"meters" => render_system_panel_meters(),
_ => render_system_panel_cards(),
}
}
fn render_system_panel_cards() -> RgbImage {
let system = collect_system_view(); let system = collect_system_view();
let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, Rgb([10, 14, 18])); 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 host_name = System::host_name().unwrap_or_else(|| "Unraid".into());
@@ -229,7 +243,10 @@ pub(crate) fn render_system_panel() -> RgbImage {
236, 236,
"System", "System",
system.uptime.clone(), system.uptime.clone(),
format!("{} CPU / {} proc", system.cpu_count, system.process_count), format!(
"{} cores | {} procs",
system.cpu_count, system.process_count
),
Rgb([216, 253, 114]), Rgb([216, 253, 114]),
), ),
]; ];
@@ -241,6 +258,109 @@ pub(crate) fn render_system_panel() -> RgbImage {
canvas canvas
} }
fn render_system_panel_meters() -> RgbImage {
let system = collect_system_view();
let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, Rgb([9, 13, 17]));
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, 72),
Rgb([15, 23, 30]),
);
draw_text_line(
&mut canvas,
Rgb([216, 253, 114]),
26,
18,
32.0,
"AOOSTAR System Monitor",
);
draw_text_line(
&mut canvas,
Rgb([146, 163, 181]),
28,
48,
18.0,
&format!("{host_name} | {timestamp}"),
);
draw_meter_row(
&mut canvas,
26,
102,
"CPU",
&format!("{}%", system.cpu_usage_percent),
parse_metric_percent(&system.cpu_usage_percent),
Rgb([110, 231, 183]),
&format!(
"Load {} / {} / {} {} cores {} procs",
system.load_avg_one,
system.load_avg_five,
system.load_avg_fifteen,
system.cpu_count,
system.process_count
),
);
draw_meter_row(
&mut canvas,
26,
170,
"RAM",
&format!("{}%", system.mem_usage_percent),
parse_metric_percent(&system.mem_usage_percent),
Rgb([120, 189, 247]),
&format!("{} / {}", system.mem_used, system.mem_total),
);
draw_meter_row(
&mut canvas,
26,
238,
"GPU",
system.temperature_gpu.as_deref().unwrap_or("-"),
normalize_temperature_bar(system.temperature_gpu.as_deref()),
Rgb([241, 132, 121]),
&format!(
"Temp meter CPU {} Cache {}% User {}%",
system.temperature_cpu.as_deref().unwrap_or("-"),
system.cache_usage_percent,
system.user_usage_percent
),
);
draw_filled_rect_mut(
&mut canvas,
Rect::at(26, 308).of_size(908, 42),
Rgb([12, 18, 24]),
);
draw_hollow_rect_mut(
&mut canvas,
Rect::at(26, 308).of_size(908, 42),
Rgb([31, 44, 58]),
);
draw_text_line(
&mut canvas,
Rgb([146, 163, 181]),
42,
321,
18.0,
&format!(
"Swap {}% {} / {} Cache {} / {} User {} / {} Uptime {}",
system.swap_usage_percent,
system.swap_used,
system.swap_total,
system.cache_used,
system.cache_total,
system.user_used,
system.user_total,
system.uptime
),
);
canvas
}
pub(crate) fn overlay_system_specs(canvas: &mut RgbImage) { pub(crate) fn overlay_system_specs(canvas: &mut RgbImage) {
let system = collect_system_view(); let system = collect_system_view();
let host_name = System::host_name().unwrap_or_else(|| "Unraid".into()); let host_name = System::host_name().unwrap_or_else(|| "Unraid".into());
@@ -309,6 +429,69 @@ pub(crate) fn overlay_system_specs(canvas: &mut RgbImage) {
); );
} }
fn draw_meter_row(
canvas: &mut RgbImage,
x: i32,
y: i32,
label: &str,
value: &str,
percent: f32,
accent: Rgb<u8>,
meta: &str,
) {
draw_text_line(canvas, accent, x, y, 19.0, label);
draw_text_line(canvas, Rgb([237, 242, 247]), x + 112, y, 19.0, value);
draw_meter_bar(canvas, x, y + 28, 840, 20, percent, accent);
draw_text_line(canvas, Rgb([146, 163, 181]), x, y + 56, 17.0, meta);
}
fn draw_meter_bar(
canvas: &mut RgbImage,
x: i32,
y: i32,
width: u32,
height: u32,
percent: f32,
accent: Rgb<u8>,
) {
draw_filled_rect_mut(
canvas,
Rect::at(x, y).of_size(width, height),
Rgb([19, 27, 35]),
);
draw_hollow_rect_mut(
canvas,
Rect::at(x, y).of_size(width, height),
Rgb([36, 50, 67]),
);
let fill_width =
((width.saturating_sub(2)) as f32 * (percent.clamp(0.0, 100.0) / 100.0)).round() as u32;
if fill_width > 0 {
draw_filled_rect_mut(
canvas,
Rect::at(x + 1, y + 1).of_size(fill_width, height.saturating_sub(2)),
accent,
);
}
}
fn parse_metric_percent(value: &str) -> f32 {
value.parse::<f32>().unwrap_or(0.0)
}
fn normalize_temperature_bar(value: Option<&str>) -> f32 {
let Some(raw) = value else {
return 0.0;
};
let numeric = raw
.replace(" °C", "")
.replace('C', "")
.trim()
.parse::<f32>()
.unwrap_or(0.0);
(numeric / 100.0 * 100.0).clamp(0.0, 100.0)
}
fn draw_metric_card( fn draw_metric_card(
canvas: &mut RgbImage, canvas: &mut RgbImage,
x: i32, x: i32,
+3
View File
@@ -47,6 +47,7 @@ pub(crate) struct RotationSnapshot {
pub(crate) custom_panel: bool, pub(crate) custom_panel: bool,
pub(crate) switch_time: u32, pub(crate) switch_time: u32,
pub(crate) specs_enabled: bool, pub(crate) specs_enabled: bool,
pub(crate) specs_mode: String,
pub(crate) memes_enabled: bool, pub(crate) memes_enabled: bool,
pub(crate) gifs_enabled: bool, pub(crate) gifs_enabled: bool,
pub(crate) active_images: Vec<String>, pub(crate) active_images: Vec<String>,
@@ -107,6 +108,7 @@ pub(crate) struct SetupView {
pub(crate) custom_panel: bool, pub(crate) custom_panel: bool,
pub(crate) switch_time: String, pub(crate) switch_time: String,
pub(crate) specs_enabled: bool, pub(crate) specs_enabled: bool,
pub(crate) specs_mode: String,
pub(crate) memes_enabled: bool, pub(crate) memes_enabled: bool,
pub(crate) gifs_enabled: bool, pub(crate) gifs_enabled: bool,
} }
@@ -173,6 +175,7 @@ pub(crate) struct ActivateRequest {
pub(crate) images: Vec<String>, pub(crate) images: Vec<String>,
pub(crate) switch_time: Option<u32>, pub(crate) switch_time: Option<u32>,
pub(crate) specs_enabled: Option<bool>, pub(crate) specs_enabled: Option<bool>,
pub(crate) specs_mode: Option<String>,
pub(crate) memes_enabled: Option<bool>, pub(crate) memes_enabled: Option<bool>,
pub(crate) gifs_enabled: Option<bool>, pub(crate) gifs_enabled: Option<bool>,
} }
+66 -9
View File
@@ -30,6 +30,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
--surface: #10161d; --surface: #10161d;
--surface-2: #141b24; --surface-2: #141b24;
--surface-3: #0c1117; --surface-3: #0c1117;
--surface-4: #172230;
--line: #223042; --line: #223042;
--line-soft: #182333; --line-soft: #182333;
--text: #eef3f8; --text: #eef3f8;
@@ -37,6 +38,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
--faint: #667789; --faint: #667789;
--accent: #6ee7b7; --accent: #6ee7b7;
--accent-2: #b9f66a; --accent-2: #b9f66a;
--accent-3: #7ec8ff;
--warn: #f6c65b; --warn: #f6c65b;
--danger: #f18479; --danger: #f18479;
--shadow: 0 18px 60px rgba(0, 0, 0, .28); --shadow: 0 18px 60px rgba(0, 0, 0, .28);
@@ -130,6 +132,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(34, 48, 66, .72);
} }
h2 { h2 {
margin: 0; margin: 0;
@@ -192,6 +196,15 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 12px; margin-top: 12px;
} }
.select-wrap select {
min-height: 42px;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--surface-3);
color: var(--text);
padding: 10px 12px;
font: inherit;
}
.toggle-pill { .toggle-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -221,6 +234,16 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
display: grid; display: grid;
align-content: space-between; align-content: space-between;
background: rgba(16, 22, 30, .86); background: rgba(16, 22, 30, .86);
position: relative;
overflow: hidden;
}
.status::after {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: linear-gradient(180deg, var(--accent), var(--accent-3));
opacity: .9;
} }
.status span, .metric-label { .status span, .metric-label {
color: var(--muted); color: var(--muted);
@@ -244,6 +267,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
.metric-card { .metric-card {
min-height: 104px; min-height: 104px;
border-left: 3px solid var(--accent); border-left: 3px solid var(--accent);
background: linear-gradient(180deg, rgba(17, 24, 32, .98), rgba(12, 17, 24, .98));
} }
.metric-card:nth-child(2) { border-left-color: #78bdf7; } .metric-card:nth-child(2) { border-left-color: #78bdf7; }
.metric-card:nth-child(3) { border-left-color: var(--warn); } .metric-card:nth-child(3) { border-left-color: var(--warn); }
@@ -376,13 +400,31 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
} }
.nyan-wrap { .nyan-wrap {
position: fixed; position: fixed;
right: 14px; right: 18px;
bottom: 10px; bottom: 18px;
width: 132px; width: 184px;
pointer-events: none; pointer-events: none;
z-index: 30; z-index: 30;
opacity: .42; opacity: .92;
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .26)); padding: 10px 12px 8px;
border: 1px solid rgba(126, 200, 255, .22);
border-radius: 8px;
background:
linear-gradient(180deg, rgba(15, 22, 30, .88), rgba(10, 15, 22, .72));
box-shadow:
0 16px 36px rgba(0, 0, 0, .28),
inset 0 0 0 1px rgba(110, 231, 183, .08);
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .22));
}
.nyan-wrap::before {
content: "UI mascot";
display: block;
margin-bottom: 6px;
color: var(--muted);
font-size: .68rem;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
} }
.nyan-wrap img { .nyan-wrap img {
display: block; display: block;
@@ -406,7 +448,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
.gallery { grid-template-columns: 1fr; } .gallery { grid-template-columns: 1fr; }
.order-item { grid-template-columns: 28px minmax(0, 1fr); } .order-item { grid-template-columns: 28px minmax(0, 1fr); }
.order-item .mini { grid-column: 2; } .order-item .mini { grid-column: 2; }
.nyan-wrap { width: 96px; opacity: .32; } .nyan-wrap { width: 128px; right: 10px; bottom: 10px; opacity: .78; padding: 8px; }
.nyan-wrap::before { margin-bottom: 4px; font-size: .62rem; }
} }
</style> </style>
</head> </head>
@@ -457,6 +500,15 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes</label> <label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes</label>
<label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs</label> <label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs</label>
</div> </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>
</div>
</div> </div>
</div> </div>
@@ -521,7 +573,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
const state = { const state = {
images: [], images: [],
activeImages: [], activeImages: [],
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, specs_mode: "cards", memes_enabled: false, gifs_enabled: true },
display: {}, display: {},
system: {}, system: {},
activeTab: "screen-control", activeTab: "screen-control",
@@ -534,6 +586,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
tabPanels: Array.from(document.querySelectorAll("[data-tab-panel]")), tabPanels: Array.from(document.querySelectorAll("[data-tab-panel]")),
switchTime: document.getElementById("switchTime"), switchTime: document.getElementById("switchTime"),
specsEnabled: document.getElementById("specsEnabled"), specsEnabled: document.getElementById("specsEnabled"),
specsMode: document.getElementById("specsMode"),
memesEnabled: document.getElementById("memesEnabled"), memesEnabled: document.getElementById("memesEnabled"),
gifsEnabled: document.getElementById("gifsEnabled"), gifsEnabled: document.getElementById("gifsEnabled"),
customPanelValue: document.getElementById("customPanelValue"), customPanelValue: document.getElementById("customPanelValue"),
@@ -677,6 +730,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
...data.setup, ...data.setup,
switch_time: el.switchTime.value || data.setup.switch_time || "10", switch_time: el.switchTime.value || data.setup.switch_time || "10",
specs_enabled: el.specsEnabled.checked, specs_enabled: el.specsEnabled.checked,
specs_mode: el.specsMode.value || data.setup.specs_mode || "cards",
memes_enabled: el.memesEnabled.checked, memes_enabled: el.memesEnabled.checked,
gifs_enabled: el.gifsEnabled.checked, gifs_enabled: el.gifsEnabled.checked,
} }
@@ -686,6 +740,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
if (!preserveDraft) { if (!preserveDraft) {
el.switchTime.value = data.setup.switch_time || "10"; el.switchTime.value = data.setup.switch_time || "10";
el.specsEnabled.checked = !!data.setup.specs_enabled; el.specsEnabled.checked = !!data.setup.specs_enabled;
el.specsMode.value = data.setup.specs_mode || "cards";
el.memesEnabled.checked = !!data.setup.memes_enabled; el.memesEnabled.checked = !!data.setup.memes_enabled;
el.gifsEnabled.checked = data.setup.gifs_enabled !== false; el.gifsEnabled.checked = data.setup.gifs_enabled !== false;
} }
@@ -712,8 +767,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
el.diskMetaValue.textContent = `${state.system.cache_used || "-"} / ${state.system.cache_total || "-"} | ${state.system.user_used || "-"} / ${state.system.user_total || "-"}`; el.diskMetaValue.textContent = `${state.system.cache_used || "-"} / ${state.system.cache_total || "-"} | ${state.system.user_used || "-"} / ${state.system.user_total || "-"}`;
el.swapUsageValue.textContent = `${state.system.swap_usage_percent || "-"} %`; el.swapUsageValue.textContent = `${state.system.swap_usage_percent || "-"} %`;
el.swapMetaValue.textContent = `${state.system.swap_used || "-"} / ${state.system.swap_total || "-"}`; el.swapMetaValue.textContent = `${state.system.swap_used || "-"} / ${state.system.swap_total || "-"}`;
el.loadValue.textContent = `${state.system.cpu_count || "-"} CPU / ${state.system.process_count || "-"} proc`; el.loadValue.textContent = `${state.system.load_avg_one || "-"} / ${state.system.load_avg_five || "-"} / ${state.system.load_avg_fifteen || "-"}`;
el.uptimeValue.textContent = `Uptime ${state.system.uptime || "-"}`; el.uptimeValue.textContent = `${state.system.cpu_count || "-"} cores • ${state.system.process_count || "-"} procs • 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.displayStatusMirror.textContent = el.displayStatusValue.textContent;
@@ -745,6 +800,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
images: state.activeImages, images: state.activeImages,
switch_time: Number.parseInt(el.switchTime.value || "10", 10), switch_time: Number.parseInt(el.switchTime.value || "10", 10),
specs_enabled: el.specsEnabled.checked, specs_enabled: el.specsEnabled.checked,
specs_mode: el.specsMode.value,
memes_enabled: el.memesEnabled.checked, memes_enabled: el.memesEnabled.checked,
gifs_enabled: el.gifsEnabled.checked, gifs_enabled: el.gifsEnabled.checked,
}), }),
@@ -869,6 +925,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
}); });
el.switchTime.addEventListener("change", markDirty); el.switchTime.addEventListener("change", markDirty);
el.specsEnabled.addEventListener("change", markDirty); el.specsEnabled.addEventListener("change", markDirty);
el.specsMode.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) => { el.tabButtons.forEach((button) => {