From 263b4edd07d761ef9d0a12875d8aa2d6e8c97f86 Mon Sep 17 00:00:00 2001 From: ModZero Date: Fri, 29 Jul 2022 04:22:21 +0200 Subject: [PATCH] Active display of a timer --- src-tauri/src/timers.rs | 113 +++++++++++++++++++++--------------- src/App.svelte | 58 +----------------- src/components/Timer.svelte | 63 ++++++++++++++++++++ src/lib/timer.ts | 111 +++++++++++++++++++++++++++++++++++ tsconfig.json | 3 + vite.config.ts | 5 ++ 6 files changed, 251 insertions(+), 102 deletions(-) create mode 100644 src/components/Timer.svelte create mode 100644 src/lib/timer.ts diff --git a/src-tauri/src/timers.rs b/src-tauri/src/timers.rs index 5ec1556..af84abf 100644 --- a/src-tauri/src/timers.rs +++ b/src-tauri/src/timers.rs @@ -13,7 +13,7 @@ use chrono::{DateTime, FixedOffset, Local}; use tauri::{ async_runtime::spawn, plugin::{Builder, TauriPlugin}, - AppHandle, Runtime, State, Manager, + AppHandle, Manager, Runtime, State, }; use tokio::time::interval; use uuid::Uuid; @@ -41,17 +41,16 @@ pub struct Timer { started: Option>, duration: Duration, elapsed: Option, - message: String, #[serde(skip)] checked: Option, + version: u64, } impl Timer { - fn new(message: &str, duration: Duration) -> Self { + fn new(duration: Duration) -> Self { Self { id: Uuid::new_v4(), duration, - message: message.to_string(), ..Default::default() } } @@ -60,28 +59,35 @@ impl Timer { self.elapsed.map_or(false, |e| e >= self.duration) } - fn start(self) -> Self { - Timer { - started: Some(Local::now().into()), - elapsed: Some(Duration::from_secs(0)), - checked: Some(Instant::now()), - ..self + fn start(&mut self) { + let now = Local::now().into(); + self.started = Some(now); + self.elapsed = Some(Duration::from_secs(0)); + self.checked = Some(Instant::now()); + self.version += 1; + } + + /// Increment the timer, returning the time since last tick + fn tick(&mut self) -> Result<(), TimerError> { + let now = Instant::now(); + match self.checked { + None => Err(TimerError::NotStarted), + Some(checked) => { + self.elapsed = Some(now - checked); + self.checked = Some(checked); + self.version += 1; + + Ok(()) + } } } - fn tick(self) -> Result { - let now = Instant::now(); - let elapsed = now - - match self.checked { - None => return Err(TimerError::NotStarted), - Some(checked) => checked, - }; - - Ok(Timer { - elapsed: self.elapsed.map(|e| e + elapsed), - checked: Some(now), - ..self - }) + fn reset(&mut self, duration: Duration) { + self.duration = duration; + self.started = None; + self.elapsed = None; + self.checked = None; + self.version += 1; } } @@ -89,8 +95,8 @@ impl Timer { struct Timers(Arc>>); impl Timers { - fn make(&self, duration: Duration, message: &str) -> Timer { - let timer = Timer::new(message, duration); + fn make(&self, duration: Duration) -> Timer { + let timer = Timer::new(duration); self.0.lock().unwrap().insert(timer.id, timer.clone()); @@ -103,34 +109,40 @@ impl Timers { fn start(&self, timer_id: Uuid) -> Result { let mut timers = self.0.lock().unwrap(); - match timers.get(&timer_id).cloned() { + match timers.get_mut(&timer_id) { None => Err(TimerError::NotFound), Some(t) => { - let started = t.start(); - timers.insert(started.id, started.clone()); + t.start(); - Ok(started) + Ok(t.clone()) } } } fn tick(&self, timer_id: Uuid) -> Result { let mut timers = self.0.lock().unwrap(); - match timers.get(&timer_id).cloned() { + match timers.get_mut(&timer_id) { + None => Err(TimerError::NotFound), + Some(t) => t.tick().and(Ok(t.clone())), + } + } + + fn reset(&self, timer_id: Uuid, duration: Duration) -> Result { + let mut timers = self.0.lock().unwrap(); + match timers.get_mut(&timer_id) { None => Err(TimerError::NotFound), Some(t) => { - let started = t.tick()?; - timers.insert(started.id, started.clone()); + t.reset(duration); - Ok(started) + Ok(t.clone()) } } } } #[tauri::command] -fn make(timers: State<'_, Timers>, duration: Duration, message: &str) -> Timer { - timers.make(duration, message) +fn make(timers: State<'_, Timers>, duration: Duration) -> Timer { + timers.make(duration) } #[tauri::command] @@ -138,6 +150,22 @@ fn delete(timers: State<'_, Timers>, timer_id: Uuid) -> Option { timers.delete(timer_id) } +#[tauri::command] +fn reset( + _app: AppHandle, + timers: State<'_, Timers>, + timer_id: Uuid, + duration: Duration, +) -> Result { + let res = timers.reset(timer_id, duration); + + if let Ok(timer) = &res { + _app.emit_all("timer-update", timer).ok(); + } + + res +} + #[tauri::command] fn start( _app: AppHandle, @@ -152,14 +180,11 @@ fn start( loop { interval.tick().await; - match timers.tick(timer_id) { - Err(_) => break, + match &timers.tick(timer_id) { + Err(_) => break, // Timer is gone or no longer running, we're done Ok(timer) => { + _app.emit_all("timer-update", timer).ok(); if timer.is_complete() { - _app.emit_all("timer-done", timer).ok(); - break; - } - if _app.emit_all("timer-tick", timer).is_err() { break; } } @@ -172,11 +197,7 @@ fn start( pub fn init() -> TauriPlugin { Builder::new("timers") - .invoke_handler(tauri::generate_handler![ - delete, - make, - start - ]) + .invoke_handler(tauri::generate_handler![delete, make, reset, start,]) .setup(|app_handle| { app_handle.manage(Timers::default()); Ok(()) diff --git a/src/App.svelte b/src/App.svelte index ac563b2..404c3be 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,56 +3,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later -->
-
-
- - -
+
diff --git a/src/components/Timer.svelte b/src/components/Timer.svelte new file mode 100644 index 0000000..315c8de --- /dev/null +++ b/src/components/Timer.svelte @@ -0,0 +1,63 @@ + + + +
+
+ {#if $timer !== null} + {$timer.elapsed ? `${$timer.elapsed.secs}.${$timer.elapsed.nanos}` : `0.0`} / + {$timer.duration.secs} + {:else} + ... + {/if} +
+ +
+ + + +
+ + diff --git a/src/lib/timer.ts b/src/lib/timer.ts new file mode 100644 index 0000000..c375eeb --- /dev/null +++ b/src/lib/timer.ts @@ -0,0 +1,111 @@ +// Copyright 2022 ModZero. +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { invoke } from "@tauri-apps/api"; +import { listen, type Event, type UnlistenFn } from "@tauri-apps/api/event"; +import { + writable, + type Readable, + type Subscriber, + type Unsubscriber, +} from "svelte/store"; + +export type Duration = { + readonly secs: number; + readonly nanos: number; +}; + +export type TimerData = { + readonly id: string; + readonly duration: Duration; + readonly elapsed: Duration | null; + readonly version: number; +}; + +export class Timer implements Readable { + private subscribers: Set> = new Set(); + + private _data: TimerData | null = null; + private get data(): TimerData | null { + return this._data; + } + private set data(v: TimerData | null) { + this._data = v; + this.subscribers.forEach((s) => s(v)); + } + + x = writable(0); + + public subscribe(run: Subscriber): Unsubscriber { + run(this.data); + this.subscribers.add(run); + + return () => { + this.subscribers.delete(run); + }; + } + + private ensureReady() { + if (!this.ready) { + throw new Error("Timer Still Processing"); + } + } + + private _duration: Duration | null; + public get duration(): Duration | null { + return this._duration; + } + + public reset(duration: Duration) { + this.ensureReady(); + invoke("plugin:timers|reset", { + timerId: this.data.id, + duration: duration, + }); + } + + private _elapsed: Duration | null; + public get elapsed(): Duration | null { + return this._elapsed; + } + + private unlistenUpdate: Promise | null = null; + + private onUpdate(event: Event): void { + if ( + event.payload.id === this.data.id && + event.payload.version >= this.data.version + ) { + this.data = event.payload; + } + } + + public get ready() { + return this.data !== null; + } + + constructor(duration: Duration) { + this.unlistenUpdate = listen("timer-update", (event) => + this.onUpdate(event) + ); + + invoke("plugin:timers|make", { + duration: duration, + }).then((timer) => { + this.data = timer; + }); + } + + public close() { + if (this.ready) { + invoke("plugin:timers|delete", { timerId: this.data.id }); + } + + this.unlistenUpdate.then((u) => u()); + } + + public start() { + this.ensureReady(); + invoke("plugin:timers|start", { timerId: this.data.id }); + } +} diff --git a/tsconfig.json b/tsconfig.json index d383031..5d6935d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,9 @@ "module": "ESNext", "resolveJsonModule": true, "baseUrl": ".", + "paths": { + "@app/*": ["./src/*"] + }, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. diff --git a/vite.config.ts b/vite.config.ts index 72509a6..da5ca57 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,4 +7,9 @@ export default defineConfig({ build: { write: true, }, + resolve: { + alias: { + "@app": "./src/", + }, + }, });