theme config, layout preview, container alignment

Server: ThemeConfig entity + CRUD (GET/PUT /theme), SQLite persistence,
ThemeUpdate broadcast to ESP32 on save and initial connect.
Client: render engine uses theme colors, full-screen redraw on theme change.
SPA: theme page with color pickers + presets, layout preview with TS port
of layout engine, justify/align controls on containers.
DisplayHint refactored to struct (kind + h_align + v_align).
This commit is contained in:
2026-06-19 03:26:18 +02:00
parent 81a4167382
commit fe59b68c37
46 changed files with 1276 additions and 118 deletions

View File

@@ -1,6 +1,6 @@
use crate::error::TcpServerError;
use domain::{BroadcastPort, DisplayHint, Layout, WidgetId, WidgetState};
use protocol::{ServerMessage, WidgetDescriptor, WireLayoutNode, encode};
use domain::{BroadcastPort, DisplayHint, Layout, ThemeConfig, WidgetId, WidgetState};
use protocol::{ServerMessage, WidgetDescriptor, WireColor, WireLayoutNode, WireTheme, encode};
use tokio::sync::broadcast;
pub struct TcpBroadcaster {
@@ -70,4 +70,26 @@ impl BroadcastPort for TcpBroadcaster {
let frame = encode(&msg).map_err(TcpServerError::Encode)?;
self.send_frame(frame)
}
async fn push_theme_update(&self, theme: &ThemeConfig) -> Result<(), Self::Error> {
let wire_theme = domain_theme_to_wire(theme);
let msg = ServerMessage::ThemeUpdate { theme: wire_theme };
let frame = encode(&msg).map_err(TcpServerError::Encode)?;
self.send_frame(frame)
}
}
pub(crate) fn domain_theme_to_wire(t: &ThemeConfig) -> WireTheme {
let c = |c: &domain::ThemeColor| WireColor {
r: c.r,
g: c.g,
b: c.b,
};
WireTheme {
primary: c(&t.primary),
secondary: c(&t.secondary),
accent: c(&t.accent),
text: c(&t.text),
background: c(&t.background),
}
}

View File

@@ -1,3 +1,4 @@
use crate::broadcaster::domain_theme_to_wire;
use crate::client_tracker::ClientTracker;
use crate::error::TcpServerError;
use domain::{ConfigRepository, WidgetStateReader};
@@ -103,11 +104,24 @@ where
widgets: wire_widgets,
};
match encode(&msg) {
Ok(frame) => Some(frame),
let mut combined = match encode(&msg) {
Ok(frame) => frame,
Err(e) => {
error!(error = %e, "failed to encode initial screen update");
None
return None;
}
};
if let Ok(Some(theme)) = config.get_theme().await {
let wire_theme = domain_theme_to_wire(&theme);
let theme_msg = ServerMessage::ThemeUpdate { theme: wire_theme };
match encode(&theme_msg) {
Ok(frame) => combined.extend_from_slice(&frame),
Err(e) => {
error!(error = %e, "failed to encode initial theme update");
}
}
}
Some(combined)
}