Keep a database of timers
This commit is contained in:
parent
bf940f418e
commit
227160e044
@ -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);
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let now = Instant::now();
|
|
||||||
let duration = now - last_checked;
|
|
||||||
|
|
||||||
elapsed += duration;
|
|
||||||
self.elapsed = Some(elapsed);
|
|
||||||
|
|
||||||
if self.complete() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if window.emit("timer-tick", self.clone()).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
last_checked = now;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window
|
fn tick(self) -> Result<Self, TimerError> {
|
||||||
.emit("timer-done", self.clone())
|
let now = Instant::now();
|
||||||
.expect("Our window went away?");
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Timers(Arc<Mutex<HashMap<Uuid, Timer>>>);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_timer(&self, timer_id: Uuid) -> Option<Timer> {
|
||||||
|
self.0.lock().unwrap().get(&timer_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
<label>
|
<div id="timer" />
|
||||||
Fire after
|
<div id="controls">
|
||||||
<input type="number" bind:value={seconds} />
|
<label>
|
||||||
</label>
|
Fire after
|
||||||
<button on:click={start_timer}>Fire!</button>
|
<input type="number" bind:value={seconds} />
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
45
src/app.css
45
src/app.css
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user