Compare commits
No commits in common. "227160e04455888ab3b644f52f2eded4c21ca232" and "3265015284a9cde15b1d2cfbd3e4c0c68cdd0f45" have entirely different histories.
227160e044
...
3265015284
@ -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. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://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
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
@ -3,34 +3,13 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::{Duration, Instant}, fmt, error::Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, Local};
|
use chrono::{DateTime, FixedOffset, Local};
|
||||||
use tauri::{State, Window, async_runtime::spawn};
|
use tauri::{async_runtime::spawn, Window};
|
||||||
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,
|
||||||
@ -38,8 +17,6 @@ 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 {
|
||||||
@ -56,119 +33,52 @@ impl Timer {
|
|||||||
self.elapsed.map_or(false, |e| e >= self.duration)
|
self.elapsed.map_or(false, |e| e >= self.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(self) -> Self {
|
async fn run(&mut self, window: Window) {
|
||||||
Timer {
|
self.started = Some(Local::now().into());
|
||||||
started: Some(Local::now().into()),
|
let mut elapsed = Duration::from_secs(0);
|
||||||
elapsed: Some(Duration::from_secs(0)),
|
self.elapsed = Some(elapsed);
|
||||||
checked: Some(Instant::now()),
|
let mut last_checked = 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);
|
let mut interval = interval(Duration::from_secs(1) / 60);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
match timers.tick_timer(timer_id) {
|
let now = Instant::now();
|
||||||
Err(_) => break,
|
let duration = now - last_checked;
|
||||||
Ok(timer) => {
|
|
||||||
if timer.complete() || window.emit("timer-tick", timer).is_err() {
|
elapsed += duration;
|
||||||
break;
|
self.elapsed = Some(elapsed);
|
||||||
}
|
|
||||||
}
|
if self.complete() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if window.emit("timer-tick", self.clone()).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_checked = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.emit("timer-done", timer).ok();
|
window
|
||||||
|
.emit("timer-done", self.clone())
|
||||||
|
.expect("Our window went away?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn start_timer(window: Window, duration: Duration, message: &str) -> Uuid {
|
||||||
|
let mut timer = Timer::new(message, duration);
|
||||||
|
let timer_id = timer.id;
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
timer.run(window).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(res)
|
timer_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(Timers(Default::default()))
|
.invoke_handler(tauri::generate_handler![start_timer])
|
||||||
.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,7 +1,3 @@
|
|||||||
<!--
|
|
||||||
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";
|
||||||
@ -9,8 +5,8 @@
|
|||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
let seconds = 5;
|
let seconds = 5;
|
||||||
let timerTickUnlisten: Promise<UnlistenFn> | null = null;
|
let timer_tick_unlisten: Promise<UnlistenFn> | null = null;
|
||||||
let timerDoneUnlisten: Promise<UnlistenFn> | null = null;
|
let timer_done_unlisten: Promise<UnlistenFn> | null = null;
|
||||||
|
|
||||||
type Timer = {
|
type Timer = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -21,58 +17,32 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
timerTickUnlisten = listen<Timer>("timer-tick", (event) => {
|
timer_tick_unlisten = listen<Timer>("timer-tick", (event) => {
|
||||||
console.log("Tick!", event.payload.id, event.payload.elapsed);
|
console.log("Tick!", event.payload.id, event.payload.elapsed);
|
||||||
});
|
});
|
||||||
|
|
||||||
timerDoneUnlisten = listen<Timer>("timer-done", (event) => {
|
timer_done_unlisten = listen<Timer>("timer-done", (event) => {
|
||||||
console.log("Done!", event.payload.id);
|
console.log("Done!", event.payload.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
timerTickUnlisten?.then((ttu) => ttu());
|
timer_tick_unlisten?.then((ttu) => ttu());
|
||||||
timerDoneUnlisten?.then((tdu) => tdu());
|
timer_done_unlisten?.then((tdu) => tdu());
|
||||||
});
|
});
|
||||||
|
|
||||||
async function startTimer() {
|
function start_timer() {
|
||||||
let timer = await invoke<Timer>("make_timer", {
|
invoke("start_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" />
|
<label>
|
||||||
<div id="controls">
|
Fire after
|
||||||
<label>
|
<input type="number" bind:value={seconds} />
|
||||||
Fire after
|
</label>
|
||||||
<input type="number" bind:value={seconds} />
|
<button on:click={start_timer}>Fire!</button>
|
||||||
</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: Helvetica, Arial, sans-serif;
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -26,6 +26,7 @@ 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;
|
||||||
@ -36,15 +37,45 @@ h1 {
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
position: absolute;
|
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 0;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: 0;
|
}
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
button {
|
||||||
right: 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
// 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