Compare commits
2 Commits
3265015284
...
227160e044
Author | SHA1 | Date | |
---|---|---|---|
227160e044 | |||
bf940f418e |
@ -1,7 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@ -658,4 +658,4 @@ specific requirements.
|
||||
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.
|
||||
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/>.
|
@ -3,13 +3,34 @@
|
||||
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 tauri::{async_runtime::spawn, Window};
|
||||
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,
|
||||
@ -17,6 +38,8 @@ struct Timer {
|
||||
duration: Duration,
|
||||
elapsed: Option<Duration>,
|
||||
message: String,
|
||||
#[serde(skip)]
|
||||
checked: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
@ -33,52 +56,119 @@ impl Timer {
|
||||
self.elapsed.map_or(false, |e| e >= self.duration)
|
||||
}
|
||||
|
||||
async fn run(&mut self, window: Window) {
|
||||
self.started = Some(Local::now().into());
|
||||
let mut elapsed = Duration::from_secs(0);
|
||||
self.elapsed = Some(elapsed);
|
||||
let mut last_checked = Instant::now();
|
||||
|
||||
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;
|
||||
fn start(self) -> Self {
|
||||
Timer {
|
||||
started: Some(Local::now().into()),
|
||||
elapsed: Some(Duration::from_secs(0)),
|
||||
checked: Some(Instant::now()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
window
|
||||
.emit("timer-done", self.clone())
|
||||
.expect("Our window went away?");
|
||||
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 start_timer(window: Window, duration: Duration, message: &str) -> Uuid {
|
||||
let mut timer = Timer::new(message, duration);
|
||||
let timer_id = timer.id;
|
||||
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 {
|
||||
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() {
|
||||
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!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
<!--
|
||||
Copyright 2022 ModZero.
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script type="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
@ -5,8 +9,8 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
let seconds = 5;
|
||||
let timer_tick_unlisten: Promise<UnlistenFn> | null = null;
|
||||
let timer_done_unlisten: Promise<UnlistenFn> | null = null;
|
||||
let timerTickUnlisten: Promise<UnlistenFn> | null = null;
|
||||
let timerDoneUnlisten: Promise<UnlistenFn> | null = null;
|
||||
|
||||
type Timer = {
|
||||
id: string;
|
||||
@ -17,32 +21,58 @@
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
timer_tick_unlisten = listen<Timer>("timer-tick", (event) => {
|
||||
timerTickUnlisten = listen<Timer>("timer-tick", (event) => {
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
timer_tick_unlisten?.then((ttu) => ttu());
|
||||
timer_done_unlisten?.then((tdu) => tdu());
|
||||
timerTickUnlisten?.then((ttu) => ttu());
|
||||
timerDoneUnlisten?.then((tdu) => tdu());
|
||||
});
|
||||
|
||||
function start_timer() {
|
||||
invoke("start_timer", {
|
||||
async function startTimer() {
|
||||
let timer = await invoke<Timer>("make_timer", {
|
||||
duration: { secs: seconds, nanos: 0 },
|
||||
message: "Hi!",
|
||||
});
|
||||
invoke("start_timer", { timerId: timer.id });
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<label>
|
||||
Fire after
|
||||
<input type="number" bind:value={seconds} />
|
||||
</label>
|
||||
<button on:click={start_timer}>Fire!</button>
|
||||
<div id="timer" />
|
||||
<div id="controls">
|
||||
<label>
|
||||
Fire after
|
||||
<input type="number" bind:value={seconds} />
|
||||
</label>
|
||||
<button on:click={startTimer}>Fire!</button>
|
||||
</div>
|
||||
</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 {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
@ -26,7 +26,6 @@ a:hover {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
@ -37,45 +36,15 @@ h1 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: absolute;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
margin: 0 0;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
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;
|
||||
}
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
// Copyright 2022 ModZero.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
|
||||
import "./app.css";
|
||||
import App from "./App.svelte";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user