diff --git a/public/icons/bell.svg b/public/icons/bell.svg
new file mode 100644
index 0000000..ec4819c
--- /dev/null
+++ b/public/icons/bell.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/fire.svg b/public/icons/fire.svg
new file mode 100644
index 0000000..cea61d6
--- /dev/null
+++ b/public/icons/fire.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/flag.svg b/public/icons/flag.svg
new file mode 100644
index 0000000..7ef620e
--- /dev/null
+++ b/public/icons/flag.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/hourglass.svg b/public/icons/hourglass.svg
new file mode 100644
index 0000000..8aca015
--- /dev/null
+++ b/public/icons/hourglass.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/icons/star.svg b/public/icons/star.svg
new file mode 100644
index 0000000..27932cc
--- /dev/null
+++ b/public/icons/star.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs
index 5f1fbe2..e69d23f 100644
--- a/src-tauri/src/app_state.rs
+++ b/src-tauri/src/app_state.rs
@@ -1,5 +1,6 @@
use crate::countdown::events::AppEvent;
use crate::countdown::service::CountdownService;
+use std::collections::HashMap;
use tokio::sync::broadcast;
#[derive(Clone, Debug)]
@@ -28,11 +29,30 @@ impl ClockAnchor {
}
}
}
+
+#[derive(Debug, Clone)]
+pub struct OverlayConfig {
+ pub icon: String,
+ pub text_color: String,
+ pub bg_color: String,
+}
+
+impl Default for OverlayConfig {
+ fn default() -> Self {
+ Self {
+ icon: String::new(),
+ text_color: "white".to_string(),
+ bg_color: "transparent".to_string(),
+ }
+ }
+}
+
#[derive(Debug)]
pub struct AppState {
pub clock_anchor: ClockAnchor,
pub countdown_service: CountdownService,
pub event_bus: broadcast::Sender,
+ pub overlay_configs: tokio::sync::Mutex>,
}
impl AppState {
@@ -42,6 +62,7 @@ impl AppState {
clock_anchor: ClockAnchor::new(),
countdown_service: CountdownService::new(),
event_bus,
+ overlay_configs: tokio::sync::Mutex::new(HashMap::new()),
}
}
}
diff --git a/src-tauri/src/countdown/commands.rs b/src-tauri/src/countdown/commands.rs
index 087fdcd..1235d74 100644
--- a/src-tauri/src/countdown/commands.rs
+++ b/src-tauri/src/countdown/commands.rs
@@ -169,6 +169,30 @@ pub async fn countdown_snapshot(
})
}
+#[command]
+pub async fn set_overlay_config(
+ state: State<'_, Arc>,
+ id: u64,
+ icon: String,
+ text_color: String,
+ bg_color: String,
+) -> Result<(), String> {
+ state
+ .overlay_configs
+ .lock()
+ .await
+ .insert(
+ id,
+ crate::app_state::OverlayConfig {
+ icon,
+ text_color,
+ bg_color,
+ },
+ );
+ let _ = state.event_bus.send(AppEvent::ConfigChanged(id));
+ Ok(())
+}
+
pub(crate) fn spawn_ticker(app: AppHandle) {
tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
diff --git a/src-tauri/src/countdown/events.rs b/src-tauri/src/countdown/events.rs
index 7519945..bd9d4f2 100644
--- a/src-tauri/src/countdown/events.rs
+++ b/src-tauri/src/countdown/events.rs
@@ -13,4 +13,5 @@ pub struct CountdownTickPayload {
pub enum AppEvent {
Tick(CountdownTickPayload),
Changed(Vec),
+ ConfigChanged(u64),
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index a63e5fd..516663a 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -9,7 +9,7 @@ use std::sync::Arc;
use crate::app_state::AppState;
use crate::countdown::commands::{
countdown_create, countdown_delete, countdown_list, countdown_pause, countdown_reset,
- countdown_resume, countdown_snapshot, countdown_start, spawn_ticker,
+ countdown_resume, countdown_snapshot, countdown_start, set_overlay_config, spawn_ticker,
};
use tauri::Manager;
@@ -29,6 +29,7 @@ pub fn run() {
countdown_resume,
countdown_pause,
countdown_snapshot,
+ set_overlay_config,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")
diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs
index cbdad4f..2f6b0a8 100644
--- a/src-tauri/src/server/mod.rs
+++ b/src-tauri/src/server/mod.rs
@@ -1,15 +1,19 @@
use crate::app_state::AppState;
+use axum::http::Method;
use axum::{routing::get, Router};
use std::sync::Arc;
+use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;
mod routes;
pub async fn start(state: Arc) {
let app = Router::new()
+ .route("/api/icons", get(routes::list_icons))
.route("/overlay/countdown", get(routes::overlay_countdown))
.route("/sse/countdown/{id}", get(routes::sse_countdown))
.nest_service("/static", ServeDir::new("public"))
+ .layer(CorsLayer::new().allow_origin(Any).allow_methods([Method::GET]))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:7420")
diff --git a/src-tauri/src/server/routes.rs b/src-tauri/src/server/routes.rs
index 7db93be..e4a2fc6 100644
--- a/src-tauri/src/server/routes.rs
+++ b/src-tauri/src/server/routes.rs
@@ -1,6 +1,7 @@
use crate::app_state::AppState;
use crate::countdown::commands::build_snapshot_dtos;
use crate::countdown::events::AppEvent;
+use axum::Json;
use axum::extract::{Path, Query, State};
use axum::response::sse::KeepAlive;
use axum::response::{
@@ -27,6 +28,13 @@ pub async fn overlay_countdown(
.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()
+ .await
+ .get(&q.id)
+ .cloned()
+ .unwrap_or_default();
let mut env = minijinja::Environment::new();
env.add_template(
@@ -37,7 +45,13 @@ pub async fn overlay_countdown(
let html = env
.get_template("page")
.unwrap()
- .render(minijinja::context! { id => q.id, remaining => remaining })
+ .render(minijinja::context! {
+ id => q.id,
+ remaining => remaining,
+ icon => config.icon,
+ text_color => config.text_color,
+ bg_color => config.bg_color,
+ })
.unwrap();
Html(html)
}
@@ -56,6 +70,9 @@ pub async fn sse_countdown(
.event("tick")
.data(format_remaining(s.duration as u64)))
}),
+ Ok(AppEvent::ConfigChanged(cid)) if cid == id => {
+ Some(Ok(Event::default().event("reload").data("")))
+ }
_ => None,
});
Sse::new(stream).keep_alive(
@@ -72,3 +89,19 @@ fn format_remaining(ms: u64) -> String {
let millis = ms % 1_000;
format!("{:02}:{:02}:{:02}.{:03}", h, m, s, millis)
}
+
+pub async fn list_icons() -> Json> {
+ let mut names = Vec::new();
+ if let Ok(mut entries) = tokio::fs::read_dir("public/icons").await {
+ while let Ok(Some(entry)) = entries.next_entry().await {
+ let path = entry.path();
+ if matches!(path.extension().and_then(|e| e.to_str()), Some("svg" | "png")) {
+ if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
+ names.push(filename.to_string());
+ }
+ }
+ }
+ }
+ names.sort();
+ Json(names)
+}
diff --git a/src-tauri/templates/overlay/countdown.html b/src-tauri/templates/overlay/countdown.html
index d826041..24a48e8 100644
--- a/src-tauri/templates/overlay/countdown.html
+++ b/src-tauri/templates/overlay/countdown.html
@@ -1,17 +1,24 @@
-
-
+
+
+
-{{ remaining }}
-
+
diff --git a/src/app/styles/countdown.css b/src/app/styles/countdown.css
index 5b88d7d..a59eeef 100644
--- a/src/app/styles/countdown.css
+++ b/src/app/styles/countdown.css
@@ -19,4 +19,56 @@
mark[data-state="Running"] { color: var(--pico-color-jade-500, #29a16a); }
mark[data-state="Paused"] { color: var(--pico-color-amber-500, #929200); }
mark[data-state="Finished"] { color: var(--pico-color-red-500, #b00020); }
-mark[data-state="Idle"] { color: var(--pico-color-zinc-400, #666666); }
\ No newline at end of file
+mark[data-state="Idle"] { color: var(--pico-color-zinc-400, #666666); }
+
+details {
+ margin-bottom: 0.75rem;
+}
+
+summary mark {
+ margin-left: 0.5rem;
+}
+
+.icon-picker {
+ display: flex;
+ gap: 0.4rem;
+ flex-wrap: wrap;
+ margin-bottom: 0.75rem;
+}
+
+.icon-btn {
+ padding: 0.3rem;
+ width: 2.5rem;
+ height: 2.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.icon-btn img {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.icon-btn.selected {
+ outline: 2px solid var(--pico-primary);
+}
+
+.overlay-colors {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ flex-wrap: wrap;
+ margin-bottom: 0.75rem;
+}
+
+.source-url {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.source-url input {
+ flex: 1;
+ font-size: 0.8rem;
+}
diff --git a/src/features/countdown/api/countdown.client.ts b/src/features/countdown/api/countdown.client.ts
index ac93352..5b6871b 100644
--- a/src/features/countdown/api/countdown.client.ts
+++ b/src/features/countdown/api/countdown.client.ts
@@ -2,6 +2,7 @@ import {invokeCommand} from "../../../shared/tauri/invoke";
import type {
CountdownCommand,
CountdownPayload,
+ OverlayConfigPayload,
CountdownSnapshot,
CountdownSnapshotDto
} from "../model/countdown.types";
@@ -50,3 +51,18 @@ export async function resetCountdown(id: number): Promise {
export async function fetchCountdownSnapshot(id: number): Promise {
return mapSnapshotDtoToSnapshot(await invokeCommand("countdown_snapshot", {id}));
}
+
+export async function setOverlayConfig(
+ id: number,
+ icon: string,
+ textColor: string,
+ bgColor: string,
+): Promise {
+ const payload: OverlayConfigPayload = {
+ id,
+ icon,
+ text_color: textColor,
+ bg_color: bgColor,
+ };
+ await invokeCommand("set_overlay_config", payload);
+}
diff --git a/src/features/countdown/components/CountdownPage.svelte b/src/features/countdown/components/CountdownPage.svelte
index ac5a469..8f8b335 100644
--- a/src/features/countdown/components/CountdownPage.svelte
+++ b/src/features/countdown/components/CountdownPage.svelte
@@ -1,12 +1,25 @@