Compare commits
4 Commits
3265015284
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
263b4edd07
|
|||
|
f67d5f6a2e
|
|||
|
227160e044
|
|||
|
bf940f418e
|
7
.vscode/tasks.json
vendored
7
.vscode/tasks.json
vendored
@@ -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"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/>.
|
||||||
@@ -1,84 +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::time::{Duration, Instant};
|
mod timers;
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, Local};
|
|
||||||
use tauri::{async_runtime::spawn, Window};
|
|
||||||
use tokio::time::interval;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Clone, Default, serde::Serialize)]
|
|
||||||
struct Timer {
|
|
||||||
id: Uuid,
|
|
||||||
started: Option<DateTime<FixedOffset>>,
|
|
||||||
duration: Duration,
|
|
||||||
elapsed: Option<Duration>,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
timer_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![start_timer])
|
.plugin(timers::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
206
src-tauri/src/timers.rs
Normal file
206
src-tauri/src/timers.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// 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, Manager, Runtime, State,
|
||||||
|
};
|
||||||
|
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>,
|
||||||
|
#[serde(skip)]
|
||||||
|
checked: Option<Instant>,
|
||||||
|
version: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer {
|
||||||
|
fn new(duration: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
duration,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_complete(&self) -> bool {
|
||||||
|
self.elapsed.map_or(false, |e| e >= self.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&mut self) {
|
||||||
|
let now = Local::now().into();
|
||||||
|
self.started = Some(now);
|
||||||
|
self.elapsed = Some(Duration::from_secs(0));
|
||||||
|
self.checked = Some(Instant::now());
|
||||||
|
self.version += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the timer, returning the time since last tick
|
||||||
|
fn tick(&mut self) -> Result<(), TimerError> {
|
||||||
|
let now = Instant::now();
|
||||||
|
match self.checked {
|
||||||
|
None => Err(TimerError::NotStarted),
|
||||||
|
Some(checked) => {
|
||||||
|
self.elapsed = Some(now - checked);
|
||||||
|
self.checked = Some(checked);
|
||||||
|
self.version += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, duration: Duration) {
|
||||||
|
self.duration = duration;
|
||||||
|
self.started = None;
|
||||||
|
self.elapsed = None;
|
||||||
|
self.checked = None;
|
||||||
|
self.version += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Timers(Arc<Mutex<HashMap<Uuid, Timer>>>);
|
||||||
|
|
||||||
|
impl Timers {
|
||||||
|
fn make(&self, duration: Duration) -> Timer {
|
||||||
|
let timer = Timer::new(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_mut(&timer_id) {
|
||||||
|
None => Err(TimerError::NotFound),
|
||||||
|
Some(t) => {
|
||||||
|
t.start();
|
||||||
|
|
||||||
|
Ok(t.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, timer_id: Uuid) -> Result<Timer, TimerError> {
|
||||||
|
let mut timers = self.0.lock().unwrap();
|
||||||
|
match timers.get_mut(&timer_id) {
|
||||||
|
None => Err(TimerError::NotFound),
|
||||||
|
Some(t) => t.tick().and(Ok(t.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&self, timer_id: Uuid, duration: Duration) -> Result<Timer, TimerError> {
|
||||||
|
let mut timers = self.0.lock().unwrap();
|
||||||
|
match timers.get_mut(&timer_id) {
|
||||||
|
None => Err(TimerError::NotFound),
|
||||||
|
Some(t) => {
|
||||||
|
t.reset(duration);
|
||||||
|
|
||||||
|
Ok(t.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn make(timers: State<'_, Timers>, duration: Duration) -> Timer {
|
||||||
|
timers.make(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn delete(timers: State<'_, Timers>, timer_id: Uuid) -> Option<Timer> {
|
||||||
|
timers.delete(timer_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn reset<R: Runtime>(
|
||||||
|
_app: AppHandle<R>,
|
||||||
|
timers: State<'_, Timers>,
|
||||||
|
timer_id: Uuid,
|
||||||
|
duration: Duration,
|
||||||
|
) -> Result<Timer, TimerError> {
|
||||||
|
let res = timers.reset(timer_id, duration);
|
||||||
|
|
||||||
|
if let Ok(timer) = &res {
|
||||||
|
_app.emit_all("timer-update", timer).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, // Timer is gone or no longer running, we're done
|
||||||
|
Ok(timer) => {
|
||||||
|
_app.emit_all("timer-update", timer).ok();
|
||||||
|
if timer.is_complete() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
|
Builder::new("timers")
|
||||||
|
.invoke_handler(tauri::generate_handler![delete, make, reset, start,])
|
||||||
|
.setup(|app_handle| {
|
||||||
|
app_handle.manage(Timers::default());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@@ -1,48 +1,23 @@
|
|||||||
|
<!--
|
||||||
|
Copyright 2022 ModZero.
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
<script type="ts">
|
<script type="ts">
|
||||||
import { onDestroy, onMount } from "svelte";
|
import Timer from "./components/Timer.svelte";
|
||||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
|
||||||
import { invoke } from "@tauri-apps/api";
|
|
||||||
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;
|
|
||||||
|
|
||||||
type Timer = {
|
|
||||||
id: string;
|
|
||||||
elapsed: {
|
|
||||||
secs: number;
|
|
||||||
nsecs: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
timer_tick_unlisten = listen<Timer>("timer-tick", (event) => {
|
|
||||||
console.log("Tick!", event.payload.id, event.payload.elapsed);
|
|
||||||
});
|
|
||||||
|
|
||||||
timer_done_unlisten = listen<Timer>("timer-done", (event) => {
|
|
||||||
console.log("Done!", event.payload.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
timer_tick_unlisten?.then((ttu) => ttu());
|
|
||||||
timer_done_unlisten?.then((tdu) => tdu());
|
|
||||||
});
|
|
||||||
|
|
||||||
function start_timer() {
|
|
||||||
invoke("start_timer", {
|
|
||||||
duration: { secs: seconds, nanos: 0 },
|
|
||||||
message: "Hi!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<label>
|
<Timer />
|
||||||
Fire after
|
|
||||||
<input type="number" bind:value={seconds} />
|
|
||||||
</label>
|
|
||||||
<button on:click={start_timer}>Fire!</button>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
46
src/app.css
46
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,14 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/components/Timer.svelte
Normal file
63
src/components/Timer.svelte
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!--
|
||||||
|
Copyright 2022 ModZero.
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
|
import { Timer } from "@app/lib/timer";
|
||||||
|
|
||||||
|
let seconds = 5;
|
||||||
|
let timer: Timer = new Timer({ secs: seconds, nanos: 0 });
|
||||||
|
$: timer.ready && timer.reset({ secs: seconds, nanos: 0 });
|
||||||
|
$: console.log(seconds);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
timer.close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="timer" />
|
||||||
|
<div class="meter">
|
||||||
|
{#if $timer !== null}
|
||||||
|
{$timer.elapsed ? `${$timer.elapsed.secs}.${$timer.elapsed.nanos}` : `0.0`} /
|
||||||
|
{$timer.duration.secs}
|
||||||
|
{:else}
|
||||||
|
...
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label>
|
||||||
|
Fire after
|
||||||
|
<input
|
||||||
|
disabled={$timer === null || !!$timer.elapsed}
|
||||||
|
type="number"
|
||||||
|
bind:value={seconds}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
disabled={$timer === null || !!$timer.elapsed}
|
||||||
|
on:click={() => timer.start()}>Fire!</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
disabled={$timer === null}
|
||||||
|
on:click={() => timer.reset({ secs: seconds, nanos: 0 })}>Reset</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.meter {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
src/lib/timer.ts
Normal file
111
src/lib/timer.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2022 ModZero.
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
import { listen, type Event, type UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import {
|
||||||
|
writable,
|
||||||
|
type Readable,
|
||||||
|
type Subscriber,
|
||||||
|
type Unsubscriber,
|
||||||
|
} from "svelte/store";
|
||||||
|
|
||||||
|
export type Duration = {
|
||||||
|
readonly secs: number;
|
||||||
|
readonly nanos: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimerData = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly duration: Duration;
|
||||||
|
readonly elapsed: Duration | null;
|
||||||
|
readonly version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Timer implements Readable<TimerData> {
|
||||||
|
private subscribers: Set<Subscriber<TimerData>> = new Set();
|
||||||
|
|
||||||
|
private _data: TimerData | null = null;
|
||||||
|
private get data(): TimerData | null {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
private set data(v: TimerData | null) {
|
||||||
|
this._data = v;
|
||||||
|
this.subscribers.forEach((s) => s(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
x = writable(0);
|
||||||
|
|
||||||
|
public subscribe(run: Subscriber<TimerData>): Unsubscriber {
|
||||||
|
run(this.data);
|
||||||
|
this.subscribers.add(run);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.subscribers.delete(run);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureReady() {
|
||||||
|
if (!this.ready) {
|
||||||
|
throw new Error("Timer Still Processing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _duration: Duration | null;
|
||||||
|
public get duration(): Duration | null {
|
||||||
|
return this._duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset(duration: Duration) {
|
||||||
|
this.ensureReady();
|
||||||
|
invoke("plugin:timers|reset", {
|
||||||
|
timerId: this.data.id,
|
||||||
|
duration: duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _elapsed: Duration | null;
|
||||||
|
public get elapsed(): Duration | null {
|
||||||
|
return this._elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unlistenUpdate: Promise<UnlistenFn> | null = null;
|
||||||
|
|
||||||
|
private onUpdate(event: Event<TimerData>): void {
|
||||||
|
if (
|
||||||
|
event.payload.id === this.data.id &&
|
||||||
|
event.payload.version >= this.data.version
|
||||||
|
) {
|
||||||
|
this.data = event.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get ready() {
|
||||||
|
return this.data !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(duration: Duration) {
|
||||||
|
this.unlistenUpdate = listen<TimerData>("timer-update", (event) =>
|
||||||
|
this.onUpdate(event)
|
||||||
|
);
|
||||||
|
|
||||||
|
invoke<TimerData>("plugin:timers|make", {
|
||||||
|
duration: duration,
|
||||||
|
}).then((timer) => {
|
||||||
|
this.data = timer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
if (this.ready) {
|
||||||
|
invoke("plugin:timers|delete", { timerId: this.data.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unlistenUpdate.then((u) => u());
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.ensureReady();
|
||||||
|
invoke("plugin:timers|start", { timerId: this.data.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
// 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";
|
||||||
|
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById("app"),
|
target: document.getElementById("app"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@app/*": ["./src/*"]
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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()],
|
||||||
build: {
|
build: {
|
||||||
write: true,
|
write: true,
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@app": "./src/",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user