Moved timers into a plugin

This commit is contained in:
Gender Shrapnel 2022-07-28 02:52:31 +02:00
parent 227160e044
commit f67d5f6a2e
Signed by: modzero
GPG Key ID: 4E11A06C6D1E5213
7 changed files with 198 additions and 171 deletions

7
.vscode/tasks.json vendored
View File

@ -28,6 +28,13 @@
"command": "npm", "command": "npm",
"problemMatcher": [], "problemMatcher": [],
"args": ["run", "build"] "args": ["run", "build"]
},
{
"label": "ui:format",
"type": "shell",
"command": "npm",
"problemMatcher": [],
"args": ["run", "format"]
} }
] ]
} }

View File

@ -1,174 +1,15 @@
// Copyright 2022 ModZero.
// SPDX-License-Identifier: AGPL-3.0-or-later
#![cfg_attr( #![cfg_attr(
all(not(debug_assertions), target_os = "windows"), all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use std::{ mod timers;
collections::HashMap,
sync::{Arc, Mutex},
time::{Duration, Instant}, fmt, error::Error,
};
use chrono::{DateTime, FixedOffset, Local};
use tauri::{State, Window, async_runtime::spawn};
use tokio::time::interval;
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)]
struct Timer {
id: Uuid,
started: Option<DateTime<FixedOffset>>,
duration: Duration,
elapsed: Option<Duration>,
message: String,
#[serde(skip)]
checked: Option<Instant>,
}
impl Timer {
fn new(message: &str, duration: Duration) -> Self {
Self {
id: Uuid::new_v4(),
duration,
message: message.to_string(),
..Default::default()
}
}
fn complete(&self) -> bool {
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 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
})
}
}
#[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]
fn make_timer(timers: State<'_, Timers>, duration: Duration, message: &str) -> Timer {
timers.make_timer(duration, message)
}
#[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 {
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();
});
Ok(res)
}
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.manage(Timers(Default::default())) .plugin(timers::init())
.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");
} }

185
src-tauri/src/timers.rs Normal file
View File

@ -0,0 +1,185 @@
// Copyright 2022 ModZero.
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::{
collections::HashMap,
error::Error,
fmt,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use chrono::{DateTime, FixedOffset, Local};
use tauri::{
async_runtime::spawn,
plugin::{Builder, TauriPlugin},
AppHandle, Runtime, State, Manager,
};
use tokio::time::interval;
use uuid::Uuid;
#[derive(serde::Serialize, Debug)]
pub 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)]
pub struct Timer {
id: Uuid,
started: Option<DateTime<FixedOffset>>,
duration: Duration,
elapsed: Option<Duration>,
message: String,
#[serde(skip)]
checked: Option<Instant>,
}
impl Timer {
fn new(message: &str, duration: Duration) -> Self {
Self {
id: Uuid::new_v4(),
duration,
message: message.to_string(),
..Default::default()
}
}
fn is_complete(&self) -> bool {
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 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
})
}
}
#[derive(Default)]
struct Timers(Arc<Mutex<HashMap<Uuid, Timer>>>);
impl Timers {
fn make(&self, duration: Duration, message: &str) -> Timer {
let timer = Timer::new(message, duration);
self.0.lock().unwrap().insert(timer.id, timer.clone());
timer
}
fn delete(&self, timer_id: Uuid) -> Option<Timer> {
self.0.lock().unwrap().get(&timer_id).cloned()
}
fn start(&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(&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]
fn make(timers: State<'_, Timers>, duration: Duration, message: &str) -> Timer {
timers.make(duration, message)
}
#[tauri::command]
fn delete(timers: State<'_, Timers>, timer_id: Uuid) -> Option<Timer> {
timers.delete(timer_id)
}
#[tauri::command]
fn start<R: Runtime>(
_app: AppHandle<R>,
timers: State<'_, Timers>,
timer_id: Uuid,
) -> Result<Timer, TimerError> {
let timers = Timers(timers.0.to_owned());
let timer = timers.start(timer_id)?;
spawn(async move {
let mut interval = interval(Duration::from_secs(1) / 60);
loop {
interval.tick().await;
match timers.tick(timer_id) {
Err(_) => break,
Ok(timer) => {
if timer.is_complete() {
_app.emit_all("timer-done", timer).ok();
break;
}
if _app.emit_all("timer-tick", timer).is_err() {
break;
}
}
}
}
});
Ok(timer)
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("timers")
.invoke_handler(tauri::generate_handler![
delete,
make,
start
])
.setup(|app_handle| {
app_handle.manage(Timers::default());
Ok(())
})
.build()
}

View File

@ -28,7 +28,6 @@
timerDoneUnlisten = 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(() => {
@ -37,11 +36,11 @@
}); });
async function startTimer() { async function startTimer() {
let timer = await invoke<Timer>("make_timer", { let timer = await invoke<Timer>("plugin:timers|make", {
duration: { secs: seconds, nanos: 0 }, duration: { secs: seconds, nanos: 0 },
message: "Hi!", message: "Hi!",
}); });
invoke("start_timer", { timerId: timer.id }); invoke("plugin:timers|start", { timerId: timer.id });
} }
</script> </script>

View File

@ -36,7 +36,6 @@ h1 {
line-height: 1.1; line-height: 1.1;
} }
#app { #app {
position: absolute; position: absolute;
max-width: 1280px; max-width: 1280px;

View File

@ -1,14 +1,11 @@
// Copyright 2022 ModZero. // Copyright 2022 ModZero.
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import "./app.css"; import "./app.css";
import App from "./App.svelte"; import App from "./App.svelte";
const app = new App({ const app = new App({
target: document.getElementById("app"), target: document.getElementById("app"),
}); });
export default app; export default app;

View File

@ -1,7 +1,6 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from "@sveltejs/vite-plugin-svelte";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [svelte()],