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 @@