feat: add overlay time format toggle and robust icon selection
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -58,4 +58,5 @@ export type OverlayConfigPayload = {
|
||||
icon: string;
|
||||
textColor: string;
|
||||
bgColor: string;
|
||||
showHhMm: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user