Compare commits

...

2 Commits

Author SHA1 Message Date
227160e044
Keep a database of timers 2022-07-28 01:47:53 +02:00
bf940f418e
Minor license stuff update 2022-07-28 01:47:36 +02:00
5 changed files with 181 additions and 88 deletions

View File

@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

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);
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");
} }

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>
<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>

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";