feat(countdown): add app state and wire countdown service into Tauri

- add `app_state` module with `ClockAnchor` and `AppState`
  - initialize `AppState` with a default `CountdownService`
  - register app state in Tauri via `.manage(app_state)`
  - extend `CountdownSnapshotDto` epoch fields to `Option<u128>`
  - add countdown model helpers for start/target epoch-ms conversion
  - return optional epoch fields from `CountdownService::snapshot`
  - ignore generated `dist/` directory in `.gitignore`
This commit is contained in:
2026-02-26 12:23:54 +01:00
parent 8517e073ba
commit 7817c9c907
6 changed files with 77 additions and 9 deletions

3
.gitignore vendored
View File

@@ -53,3 +53,6 @@ pids
debug.log
TODO.md
.aider*
# dist folder
dist/

View File

@@ -0,0 +1,37 @@
use crate::countdown::service::CountdownService;
#[derive(Clone, Debug)]
pub struct ClockAnchor {
pub boot_instant: std::time::Instant,
pub boot_epoch_ms: u128,
}
impl ClockAnchor {
pub fn new() -> Self {
Self {
boot_instant: std::time::Instant::now(),
boot_epoch_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
}
}
pub fn instant_to_epoch_ms(&self, instant: std::time::Instant) -> u128 {
instant.duration_since(self.boot_instant).as_millis() + self.boot_epoch_ms
}
}
#[derive(Debug)]
pub struct AppState {
pub clock_anchor: ClockAnchor,
pub countdowns: Vec<CountdownService>,
}
impl AppState {
pub fn new() -> Self {
Self {
clock_anchor: ClockAnchor::new(),
countdowns: vec![CountdownService::new()],
}
}
}

View File

@@ -1,4 +1,4 @@
use tokio::time::{Duration, Instant};
use tokio::time::Duration;
use crate::countdown::model::CountdownState;
@@ -8,8 +8,8 @@ pub struct CountdownSnapshotDto {
pub label: String,
pub duration: Duration,
pub state: CountdownState,
pub start_epoch_ms: Option<u64>,
pub target_epoch_ms: Option<u64>,
pub start_epoch_ms: Option<u128>,
pub target_epoch_ms: Option<u128>,
}
impl CountdownSnapshotDto {
@@ -18,8 +18,8 @@ impl CountdownSnapshotDto {
label: String,
duration: Duration,
state: CountdownState,
start_epoch_ms: Option<u64>,
target_epoch_ms: Option<u64>,
start_epoch_ms: Option<u128>,
target_epoch_ms: Option<u128>,
) -> Self {
Self {
id,

View File

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -164,6 +165,29 @@ impl Countdown {
self.start_timestamp = None;
self.target_timestamp = None;
}
pub fn start_epoch_ms(&self) -> u128 {
let anchor_instant = Instant::now();
let anchor_epoch_ms = SystemTime::now();
let anchor_epoch_ms = anchor_epoch_ms.duration_since(UNIX_EPOCH).unwrap();
anchor_instant
.checked_duration_since(self.start_timestamp.unwrap())
.unwrap()
.as_millis()
- anchor_epoch_ms.as_millis()
}
pub fn target_epoch_ms(&self) -> u128 {
let anchor_instant = Instant::now();
let anchor_epoch_ms = SystemTime::now();
let anchor_epoch_ms = anchor_epoch_ms.duration_since(UNIX_EPOCH).unwrap();
self.target_timestamp
.unwrap()
.checked_duration_since(anchor_instant)
.unwrap()
.as_millis()
+ anchor_epoch_ms.as_millis()
}
}
#[cfg(test)]

View File

@@ -1,10 +1,10 @@
use chrono::Utc;
use tokio::sync::Mutex;
use tokio::time::{Duration, Instant};
use crate::countdown::dto::CountdownSnapshotDto;
use crate::countdown::model::{Countdown, CountdownError};
#[derive(Debug)]
pub struct CountdownService {
countdown: Mutex<Countdown>,
next_id: u64,
@@ -20,14 +20,13 @@ impl CountdownService {
pub async fn snapshot(&self, now: Instant) -> CountdownSnapshotDto {
let countdown = self.countdown.lock().await;
let instant_now = Instant::now();
CountdownSnapshotDto {
id: countdown.id,
label: countdown.label.to_string(),
state: countdown.state(),
duration: countdown.remaining(),
start_epoch_ms: countdown.start_epoch_ms(),
target_epoch_ms: countdown.target_epoch_ms(),
start_epoch_ms: Some(countdown.start_epoch_ms()),
target_epoch_ms: Some(countdown.target_epoch_ms()),
}
}

View File

@@ -1,5 +1,8 @@
mod app_state;
mod countdown;
pub use app_state::AppState;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
@@ -8,7 +11,9 @@ fn greet(name: &str) -> String {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let app_state = AppState::new();
tauri::Builder::default()
.manage(app_state)
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())