Boot splash and periodic announcement banner

Splash: resources/splash/splash.gif built into the binary, played once by
SplashOverlay over a black cover present from the first frame (no UI
flash), then fades into the UI; hides icons loading. GIF, so no
QtMultimedia/gstreamer. Toggle: splash.enabled.

Banner (InfoBanner): metro-style band that fades/slides in with a hold
pictogram; text that does not fit runs as a seamless ticker for two
rounds. Never takes touches; hidden during splash and settings. Config
section banner (enabled, text, intervalSec, showSec, position,
accentColor), editable in the web UI and on-device settings; shown once
after saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-10 10:58:16 +00:00
co-authored by Claude Opus 5.5
parent ec84133417
commit f07e153f09
22 changed files with 753 additions and 3 deletions
+22
View File
@@ -548,6 +548,28 @@ $("#save-feedback")?.addEventListener("click", async () => {
} catch (e) { flash(e.message, true); }
});
// ---------- Banner + splash ----------
function syncBannerCount() {
const t = $("#banner-text"), c = $("#banner-text-count");
if (t && c) c.textContent = t.value.length;
}
$("#banner-text")?.addEventListener("input", syncBannerCount);
syncBannerCount();
$("#save-banner")?.addEventListener("click", async () => {
try {
await api("PUT", "/api/banner", {
splashEnabled: $("#splash-enabled").checked,
enabled: $("#banner-enabled").checked,
text: $("#banner-text").value,
intervalSec: parseInt($("#banner-interval").value, 10),
showSec: parseInt($("#banner-show").value, 10),
position: $("#banner-position").value,
accentColor: $("#banner-accent").value,
});
flash("Объявление сохранено");
} catch (e) { flash(e.message, true); }
});
// ---------- Icons ----------
$("#upload-icon")?.addEventListener("click", async () => {
const file = $("#icon-file").files[0];
+12 -2
View File
@@ -105,6 +105,16 @@ main {
label { display: flex; flex-direction: column; gap: 5px; font-size: 0.88em; color: var(--muted); }
label.check { flex-direction: row; align-items: center; gap: 8px; color: var(--text); }
label.label-range { gap: 6px; }
.banner-text-label { margin-bottom: 14px; }
.banner-text-label textarea {
width: 100%;
box-sizing: border-box;
min-height: 4.5em;
resize: vertical;
font: inherit;
font-size: 1.1em;
line-height: 1.4;
}
.label-caption {
display: block;
font-size: 0.88em;
@@ -117,7 +127,7 @@ label.label-range { gap: 6px; }
font-weight: 600;
}
input[type="text"], input[type="number"], input[type="password"], select {
input[type="text"], input[type="number"], input[type="password"], select, textarea {
background: var(--bg-input);
border: 1px solid var(--border-strong);
color: var(--text);
@@ -126,7 +136,7 @@ input[type="text"], input[type="number"], input[type="password"], select {
font: inherit;
width: 100%;
}
input:focus, select:focus { border-color: var(--accent); outline: none; }
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; }
input[type="color"] {
width: 48px; height: 34px; padding: 2px;
background: var(--bg-input); border: 1px solid var(--border-strong);