Active display of a timer
This commit is contained in:
parent
f67d5f6a2e
commit
263b4edd07
@ -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<DateTime<FixedOffset>>,
|
||||
duration: Duration,
|
||||
elapsed: Option<Duration>,
|
||||
message: String,
|
||||
#[serde(skip)]
|
||||
checked: Option<Instant>,
|
||||
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<Self, TimerError> {
|
||||
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<Mutex<HashMap<Uuid, Timer>>>);
|
||||
|
||||
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<Timer, TimerError> {
|
||||
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<Timer, TimerError> {
|
||||
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<Timer, TimerError> {
|
||||
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<Timer> {
|
||||
timers.delete(timer_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn reset<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
timers: State<'_, Timers>,
|
||||
timer_id: Uuid,
|
||||
duration: Duration,
|
||||
) -> Result<Timer, TimerError> {
|
||||
let res = timers.reset(timer_id, duration);
|
||||
|
||||
if let Ok(timer) = &res {
|
||||
_app.emit_all("timer-update", timer).ok();
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
@ -152,14 +180,11 @@ fn start<R: Runtime>(
|
||||
|
||||
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<R: Runtime>(
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
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(())
|
||||
|
@ -3,56 +3,11 @@
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script type="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
let seconds = 5;
|
||||
let timerTickUnlisten: Promise<UnlistenFn> | null = null;
|
||||
let timerDoneUnlisten: Promise<UnlistenFn> | null = null;
|
||||
|
||||
type Timer = {
|
||||
id: string;
|
||||
elapsed: {
|
||||
secs: number;
|
||||
nsecs: number;
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
timerTickUnlisten = listen<Timer>("timer-tick", (event) => {
|
||||
console.log("Tick!", event.payload.id, event.payload.elapsed);
|
||||
});
|
||||
|
||||
timerDoneUnlisten = listen<Timer>("timer-done", (event) => {
|
||||
console.log("Done!", event.payload.id);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
timerTickUnlisten?.then((ttu) => ttu());
|
||||
timerDoneUnlisten?.then((tdu) => tdu());
|
||||
});
|
||||
|
||||
async function startTimer() {
|
||||
let timer = await invoke<Timer>("plugin:timers|make", {
|
||||
duration: { secs: seconds, nanos: 0 },
|
||||
message: "Hi!",
|
||||
});
|
||||
invoke("plugin:timers|start", { timerId: timer.id });
|
||||
}
|
||||
import Timer from "./components/Timer.svelte";
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div id="timer" />
|
||||
<div id="controls">
|
||||
<label>
|
||||
Fire after
|
||||
<input type="number" bind:value={seconds} />
|
||||
</label>
|
||||
<button on:click={startTimer}>Fire!</button>
|
||||
</div>
|
||||
<Timer />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@ -65,13 +20,4 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#timer {
|
||||
flex-grow: 1;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#controls {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
63
src/components/Timer.svelte
Normal file
63
src/components/Timer.svelte
Normal file
@ -0,0 +1,63 @@
|
||||
<!--
|
||||
Copyright 2022 ModZero.
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
import { Timer } from "@app/lib/timer";
|
||||
|
||||
let seconds = 5;
|
||||
let timer: Timer = new Timer({ secs: seconds, nanos: 0 });
|
||||
$: timer.ready && timer.reset({ secs: seconds, nanos: 0 });
|
||||
$: console.log(seconds);
|
||||
|
||||
onDestroy(() => {
|
||||
timer.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="timer" />
|
||||
<div class="meter">
|
||||
{#if $timer !== null}
|
||||
{$timer.elapsed ? `${$timer.elapsed.secs}.${$timer.elapsed.nanos}` : `0.0`} /
|
||||
{$timer.duration.secs}
|
||||
{:else}
|
||||
...
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<label>
|
||||
Fire after
|
||||
<input
|
||||
disabled={$timer === null || !!$timer.elapsed}
|
||||
type="number"
|
||||
bind:value={seconds}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
disabled={$timer === null || !!$timer.elapsed}
|
||||
on:click={() => timer.start()}>Fire!</button
|
||||
>
|
||||
<button
|
||||
disabled={$timer === null}
|
||||
on:click={() => timer.reset({ secs: seconds, nanos: 0 })}>Reset</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.meter {
|
||||
flex-grow: 1;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
111
src/lib/timer.ts
Normal file
111
src/lib/timer.ts
Normal file
@ -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<TimerData> {
|
||||
private subscribers: Set<Subscriber<TimerData>> = 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<TimerData>): 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<UnlistenFn> | null = null;
|
||||
|
||||
private onUpdate(event: Event<TimerData>): 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<TimerData>("timer-update", (event) =>
|
||||
this.onUpdate(event)
|
||||
);
|
||||
|
||||
invoke<TimerData>("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 });
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -7,4 +7,9 @@ export default defineConfig({
|
||||
build: {
|
||||
write: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": "./src/",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user