feat: add overlay time format toggle and robust icon selection

This commit is contained in:
2026-03-06 16:54:34 +01:00
parent 52a41b9564
commit cf3c069edb
6 changed files with 94 additions and 39 deletions

View File

@@ -35,6 +35,7 @@ pub struct OverlayConfig {
pub icon: String,
pub text_color: String,
pub bg_color: String,
pub show_hh_mm: bool,
}
impl Default for OverlayConfig {
@@ -43,6 +44,7 @@ impl Default for OverlayConfig {
icon: String::new(),
text_color: "white".to_string(),
bg_color: "transparent".to_string(),
show_hh_mm: false,
}
}
}

View File

@@ -176,6 +176,7 @@ pub async fn set_overlay_config(
icon: String,
text_color: String,
bg_color: String,
show_hh_mm: bool,
) -> Result<(), String> {
state
.overlay_configs
@@ -187,6 +188,7 @@ pub async fn set_overlay_config(
icon,
text_color,
bg_color,
show_hh_mm,
},
);
let _ = state.event_bus.send(AppEvent::ConfigChanged(id));

View File

@@ -22,12 +22,6 @@ pub async fn overlay_countdown(
State(state): State<Arc<AppState>>,
Query(q): Query<OverlayQuery>,
) -> Html<String> {
let remaining = build_snapshot_dtos(&state)
.await
.ok()
.and_then(|snaps| snaps.into_iter().find(|s| s.id == q.id))
.map(|s| format_remaining(s.duration as u64))
.unwrap_or_else(|| "??:??:??.???".to_string());
let config = state
.overlay_configs
.lock()
@@ -35,6 +29,12 @@ pub async fn overlay_countdown(
.get(&q.id)
.cloned()
.unwrap_or_default();
let remaining = build_snapshot_dtos(&state)
.await
.ok()
.and_then(|snaps| snaps.into_iter().find(|s| s.id == q.id))
.map(|s| format_remaining(s.duration as u64, config.show_hh_mm))
.unwrap_or_else(|| format_unknown(config.show_hh_mm));
let mut env = minijinja::Environment::new();
env.add_template(
@@ -60,15 +60,22 @@ pub async fn sse_countdown(
State(state): State<Arc<AppState>>,
Path(id): Path<u64>,
) -> Sse<impl futures_core::Stream<Item = Result<Event, axum::Error>>> {
let show_hh_mm = state
.overlay_configs
.lock()
.await
.get(&id)
.map(|cfg| cfg.show_hh_mm)
.unwrap_or(false);
let rx = state.event_bus.subscribe();
let stream = BroadcastStream::new(rx).filter_map(move |event| match event {
Ok(AppEvent::Tick(p)) if p.id == id => Some(Ok(Event::default()
.event("tick")
.data(format_remaining(p.remaining_ms)))),
.data(format_remaining(p.remaining_ms, show_hh_mm)))),
Ok(AppEvent::Changed(snaps)) => snaps.iter().find(|s| s.id == id).map(|s| {
Ok(Event::default()
.event("tick")
.data(format_remaining(s.duration as u64)))
.data(format_remaining(s.duration as u64, show_hh_mm)))
}),
Ok(AppEvent::ConfigChanged(cid)) if cid == id => {
Some(Ok(Event::default().event("reload").data("")))
@@ -82,12 +89,23 @@ pub async fn sse_countdown(
)
}
fn format_remaining(ms: u64) -> String {
fn format_remaining(ms: u64, show_hh_mm: bool) -> String {
let total_seconds = ms / 1_000;
if !show_hh_mm {
return total_seconds.to_string();
}
let h = ms / 3_600_000;
let m = (ms % 3_600_000) / 60_000;
let s = (ms % 60_000) / 1_000;
let millis = ms % 1_000;
format!("{:02}:{:02}:{:02}.{:03}", h, m, s, millis)
format!("{:02}:{:02}:{:02}", h, m, s)
}
fn format_unknown(show_hh_mm: bool) -> String {
if show_hh_mm {
"??:??:??".to_string()
} else {
"??".to_string()
}
}
pub async fn list_icons() -> Json<Vec<String>> {

View File

@@ -57,12 +57,14 @@ export async function setOverlayConfig(
icon: string,
textColor: string,
bgColor: string,
showHhMm: boolean,
): Promise<void> {
const payload: OverlayConfigPayload = {
id,
icon,
textColor,
bgColor,
showHhMm,
};
await invokeCommand<void>("set_overlay_config", payload);
}

View File

@@ -11,6 +11,7 @@
textColor: string;
bgColor: string;
bgTransparent: boolean;
showHHMM: boolean;
};
let label = "";
@@ -39,6 +40,7 @@
textColor: "#ffffff",
bgColor: "#000000",
bgTransparent: true,
showHHMM: false,
},
};
}
@@ -57,7 +59,13 @@
function pushConfig(id: number) {
const s = getSettings(id);
void setOverlayConfig(id, s.icon, s.textColor, s.bgTransparent ? "transparent" : s.bgColor);
void setOverlayConfig(
id,
s.icon,
s.textColor,
s.bgTransparent ? "transparent" : s.bgColor,
s.showHHMM,
).catch((error) => console.error(error));
}
async function copyUrl(id: number) {
@@ -93,7 +101,7 @@
<input aria-label="Hours" bind:value={hours} max="99" min="0" placeholder="hh" type="number"/>
<input aria-label="Minutes" bind:value={minutes} max="59" min="0" placeholder="mm" type="number"/>
<input aria-label="Seconds" bind:value={seconds} max="59" min="0" placeholder="ss" type="number"/>
<button on:click={handleCreate}>Create</button>
<button type="button" on:click={handleCreate}>Create</button>
</fieldset>
</article>
@@ -103,9 +111,9 @@
<summary>
{#if getSettings(item.id).icon}
<img
src={`${OVERLAY_SERVER_ORIGIN}/static/icons/${getSettings(item.id).icon}`}
alt={getSettings(item.id).icon}
style="width:1.2em;height:1.2em;vertical-align:middle;margin-right:0.3em;"
src={`${OVERLAY_SERVER_ORIGIN}/static/icons/${getSettings(item.id).icon}`}
alt={getSettings(item.id).icon}
style="width:1.2em;height:1.2em;vertical-align:middle;margin-right:0.3em;"
/>
{/if}
{item.label}
@@ -122,34 +130,43 @@
<div class="countdown-actions">
{#if item.state === "Idle"}
<button on:click={() => selectAndRun(item.id, countdownStore.startSelected)}>Start</button>
<button type="button" on:click={() => selectAndRun(item.id, countdownStore.startSelected)}>Start
</button>
{:else if item.state === "Running"}
<button on:click={() => selectAndRun(item.id, countdownStore.pauseSelected)}>Pause</button>
<button type="button" on:click={() => selectAndRun(item.id, countdownStore.pauseSelected)}>Pause
</button>
{:else if item.state === "Paused"}
<button on:click={() => selectAndRun(item.id, countdownStore.resumeSelected)}>Resume</button>
<button type="button" on:click={() => selectAndRun(item.id, countdownStore.resumeSelected)}>Resume
</button>
{/if}
<button class="secondary" on:click={() => selectAndRun(item.id, countdownStore.resetSelected)}>Reset</button>
<button class="secondary contrast" on:click={() => selectAndRun(item.id, countdownStore.deleteSelected)}>Delete</button>
<button type="button" class="secondary"
on:click={() => selectAndRun(item.id, countdownStore.resetSelected)}>Reset
</button>
<button type="button" class="secondary contrast"
on:click={() => selectAndRun(item.id, countdownStore.deleteSelected)}>Delete
</button>
</div>
<hr />
<hr/>
<p><small>Icon</small></p>
<div class="icon-picker">
{#each icons as name}
<button
class="icon-btn {getSettings(item.id).icon === name ? 'selected' : ''}"
on:click={() => {
type="button"
class="icon-btn {getSettings(item.id).icon === name ? 'selected' : ''}"
on:click|stopPropagation|preventDefault={() => {
updateSettings(item.id, {icon: name});
pushConfig(item.id);
}}
>
<img src={`${OVERLAY_SERVER_ORIGIN}/static/icons/${name}`} alt={name} />
<img src={`${OVERLAY_SERVER_ORIGIN}/static/icons/${name}`} alt={name}/>
</button>
{/each}
<button
class="icon-btn"
on:click={() => {
type="button"
class="icon-btn"
on:click|stopPropagation|preventDefault={() => {
updateSettings(item.id, {icon: ''});
pushConfig(item.id);
}}
@@ -162,9 +179,9 @@
<label>
Text
<input
type="color"
value={getSettings(item.id).textColor}
on:change={(e) => {
type="color"
value={getSettings(item.id).textColor}
on:change={(e) => {
updateSettings(item.id, {textColor: e.currentTarget.value});
pushConfig(item.id);
}}
@@ -172,9 +189,9 @@
</label>
<label>
<input
type="checkbox"
checked={getSettings(item.id).bgTransparent}
on:change={(e) => {
type="checkbox"
checked={getSettings(item.id).bgTransparent}
on:change={(e) => {
updateSettings(item.id, {bgTransparent: e.currentTarget.checked});
pushConfig(item.id);
}}
@@ -185,20 +202,33 @@
<label>
BG
<input
type="color"
value={getSettings(item.id).bgColor}
on:change={(e) => {
type="color"
value={getSettings(item.id).bgColor}
on:change={(e) => {
updateSettings(item.id, {bgColor: e.currentTarget.value});
pushConfig(item.id);
}}
/>
</label>
{/if}
<br/>
</div>
<div>
<label>
<input
type="checkbox"
checked={getSettings(item.id).showHHMM}
on:change={(e) => {
updateSettings(item.id, {showHHMM: e.currentTarget.checked});
pushConfig(item.id);
}}
/>
Show HH:MM
</label>
</div>
<div class="source-url">
<input readonly value={`${OVERLAY_SERVER_ORIGIN}/overlay/countdown?id=${item.id}`} />
<button class="secondary" on:click={() => copyUrl(item.id)}>Copy</button>
<input readonly value={`${OVERLAY_SERVER_ORIGIN}/overlay/countdown?id=${item.id}`}/>
<button type="button" class="secondary" on:click={() => copyUrl(item.id)}>Copy</button>
</div>
</details>
</article>

View File

@@ -58,4 +58,5 @@ export type OverlayConfigPayload = {
icon: string;
textColor: string;
bgColor: string;
showHhMm: boolean;
};