Keep a database of timers

This commit is contained in:
Gender Shrapnel 2022-07-28 01:47:53 +02:00
parent bf940f418e
commit 227160e044
Signed by: modzero
GPG Key ID: 4E11A06C6D1E5213
4 changed files with 178 additions and 85 deletions

View File

@ -3,13 +3,34 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use std::time::{Duration, Instant}; use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::{Duration, Instant}, fmt, error::Error,
};
use chrono::{DateTime, FixedOffset, Local}; use chrono::{DateTime, FixedOffset, Local};
use tauri::{async_runtime::spawn, Window}; use tauri::{State, Window, async_runtime::spawn};
use tokio::time::interval; use tokio::time::interval;
use uuid::Uuid; use uuid::Uuid;
#[derive(serde::Serialize, Debug)]
enum TimerError {
NotFound,
NotStarted,
}
impl fmt::Display for TimerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimerError::NotFound => write!(f, "timer not found"),
TimerError::NotStarted => write!(f, "timer not started"),
}
}
}
impl Error for TimerError {}
#[derive(Clone, Default, serde::Serialize)] #[derive(Clone, Default, serde::Serialize)]
struct Timer { struct Timer {
id: Uuid, id: Uuid,
@ -17,6 +38,8 @@ struct Timer {
duration: Duration, duration: Duration,
elapsed: Option<Duration>, elapsed: Option<Duration>,
message: String, message: String,
#[serde(skip)]
checked: Option<Instant>,
} }
impl Timer { impl Timer {
@ -33,52 +56,119 @@ impl Timer {
self.elapsed.map_or(false, |e| e >= self.duration) self.elapsed.map_or(false, |e| e >= self.duration)
} }
async fn run(&mut self, window: Window) { fn start(self) -> Self {
self.started = Some(Local::now().into()); Timer {
let mut elapsed = Duration::from_secs(0); started: Some(Local::now().into()),
self.elapsed = Some(elapsed); elapsed: Some(Duration::from_secs(0)),
let mut last_checked = Instant::now(); checked: Some(Instant::now()),
..self
}
}
let mut interval = interval(Duration::from_secs(1) / 60); fn tick(self) -> Result<Self, TimerError> {
loop {
interval.tick().await;
let now = Instant::now(); let now = Instant::now();
let duration = now - last_checked; let elapsed = now - match self.checked {
None => return Err(TimerError::NotStarted),
Some(checked) => checked,
};
elapsed += duration; Ok(Timer {
self.elapsed = Some(elapsed); elapsed: self.elapsed.map(|e| e + elapsed),
checked: Some(now),
if self.complete() { ..self
break; })
}
} }
if window.emit("timer-tick", self.clone()).is_err() { #[derive(Default)]
break; struct Timers(Arc<Mutex<HashMap<Uuid, Timer>>>);
}
last_checked = now; impl Timers {
fn make_timer(&self, duration: Duration, message: &str) -> Timer {
let timer = Timer::new(message, duration);
self.0.lock().unwrap().insert(timer.id, timer.clone());
timer
} }
window fn delete_timer(&self, timer_id: Uuid) -> Option<Timer> {
.emit("timer-done", self.clone()) self.0.lock().unwrap().get(&timer_id).cloned()
.expect("Our window went away?");
} }
fn start_timer(&self, timer_id: Uuid) -> Result<Timer, TimerError> {
let mut timers = self.0.lock().unwrap();
match timers.get(&timer_id).cloned() {
None => Err(TimerError::NotFound),
Some(t) => {
let started = t.start();
timers.insert(started.id, started.clone());
Ok(started)
}
}
}
fn tick_timer(&self, timer_id: Uuid) -> Result<Timer, TimerError> {
let mut timers = self.0.lock().unwrap();
match timers.get(&timer_id).cloned() {
None => Err(TimerError::NotFound),
Some(t) => {
let started = t.tick()?;
timers.insert(started.id, started.clone());
Ok(started)
}
}
}
} }
#[tauri::command] #[tauri::command]
fn start_timer(window: Window, duration: Duration, message: &str) -> Uuid { fn make_timer(timers: State<'_, Timers>, duration: Duration, message: &str) -> Timer {
let mut timer = Timer::new(message, duration); timers.make_timer(duration, message)
let timer_id = timer.id; }
#[tauri::command]
fn delete_timer(timers: State<'_, Timers>, timer_id: Uuid) -> Option<Timer> {
timers.delete_timer(timer_id)
}
#[tauri::command]
fn start_timer(window: Window, timers: State<'_, Timers>, timer_id: Uuid) -> Result<Timer, TimerError> {
let timers = Timers(timers.0.to_owned());
let timer = timers.start_timer(timer_id)?;
let res = timer.clone();
spawn(async move { spawn(async move {
timer.run(window).await; let mut interval = interval(Duration::from_secs(1) / 60);
loop {
interval.tick().await;
match timers.tick_timer(timer_id) {
Err(_) => break,
Ok(timer) => {
if timer.complete() || window.emit("timer-tick", timer).is_err() {
break;
}
}
}
}
window.emit("timer-done", timer).ok();
}); });
timer_id Ok(res)
} }
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![start_timer]) .manage(Timers(Default::default()))
.invoke_handler(tauri::generate_handler![
delete_timer,
make_timer,
start_timer
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,3 +1,7 @@
<!--
Copyright 2022 ModZero.
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script type="ts"> <script type="ts">
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import type { UnlistenFn } from "@tauri-apps/api/event"; import type { UnlistenFn } from "@tauri-apps/api/event";
@ -5,8 +9,8 @@
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
let seconds = 5; let seconds = 5;
let timer_tick_unlisten: Promise<UnlistenFn> | null = null; let timerTickUnlisten: Promise<UnlistenFn> | null = null;
let timer_done_unlisten: Promise<UnlistenFn> | null = null; let timerDoneUnlisten: Promise<UnlistenFn> | null = null;
type Timer = { type Timer = {
id: string; id: string;
@ -17,32 +21,58 @@
}; };
onMount(() => { onMount(() => {
timer_tick_unlisten = listen<Timer>("timer-tick", (event) => { timerTickUnlisten = listen<Timer>("timer-tick", (event) => {
console.log("Tick!", event.payload.id, event.payload.elapsed); console.log("Tick!", event.payload.id, event.payload.elapsed);
}); });
timer_done_unlisten = listen<Timer>("timer-done", (event) => { timerDoneUnlisten = listen<Timer>("timer-done", (event) => {
console.log("Done!", event.payload.id); console.log("Done!", event.payload.id);
}); });
}); });
onDestroy(() => { onDestroy(() => {
timer_tick_unlisten?.then((ttu) => ttu()); timerTickUnlisten?.then((ttu) => ttu());
timer_done_unlisten?.then((tdu) => tdu()); timerDoneUnlisten?.then((tdu) => tdu());
}); });
function start_timer() { async function startTimer() {
invoke("start_timer", { let timer = await invoke<Timer>("make_timer", {
duration: { secs: seconds, nanos: 0 }, duration: { secs: seconds, nanos: 0 },
message: "Hi!", message: "Hi!",
}); });
invoke("start_timer", { timerId: timer.id });
} }
</script> </script>
<main> <main>
<div id="timer" />
<div id="controls">
<label> <label>
Fire after Fire after
<input type="number" bind:value={seconds} /> <input type="number" bind:value={seconds} />
</label> </label>
<button on:click={start_timer}>Fire!</button> <button on:click={startTimer}>Fire!</button>
</div>
</main> </main>
<style>
main {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
display: flex;
flex-direction: column;
}
#timer {
flex-grow: 1;
margin: 0.5em;
}
#controls {
margin: 0.5em;
padding: 0.5em;
}
</style>

View File

@ -1,5 +1,5 @@
:root { :root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-family: Helvetica, Arial, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
font-weight: 400; font-weight: 400;
@ -26,7 +26,6 @@ a:hover {
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center; place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
@ -37,45 +36,15 @@ h1 {
line-height: 1.1; line-height: 1.1;
} }
.card {
padding: 2em;
}
#app { #app {
position: absolute;
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 0;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
} top: 0;
bottom: 0;
button { left: 0;
border-radius: 8px; right: 0;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
} }

View File

@ -1,3 +1,7 @@
// Copyright 2022 ModZero.
// SPDX-License-Identifier: AGPL-3.0-or-later
import "./app.css"; import "./app.css";
import App from "./App.svelte"; import App from "./App.svelte";