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", | ||||
|       "problemMatcher": [], | ||||
|       "args": ["run", "build"] | ||||
|     }, | ||||
|     { | ||||
|       "label": "ui:format", | ||||
|       "type": "shell", | ||||
|       "command": "npm", | ||||
|       "problemMatcher": [], | ||||
|       "args": ["run", "format"] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -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/>. | ||||
| @@ -1,84 +1,15 @@ | ||||
| // Copyright 2022 ModZero. | ||||
| // SPDX-License-Identifier: 	AGPL-3.0-or-later | ||||
| #![cfg_attr( | ||||
|     all(not(debug_assertions), target_os = "windows"), | ||||
|     windows_subsystem = "windows" | ||||
| )] | ||||
|  | ||||
| use std::time::{Duration, Instant}; | ||||
|  | ||||
| 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 | ||||
| } | ||||
| mod timers; | ||||
|  | ||||
| fn main() { | ||||
|     tauri::Builder::default() | ||||
|         .invoke_handler(tauri::generate_handler![start_timer]) | ||||
|         .plugin(timers::init()) | ||||
|         .run(tauri::generate_context!()) | ||||
|         .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"> | ||||
|   import { onDestroy, onMount } from "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!", | ||||
|     }); | ||||
|   } | ||||
|   import Timer from "./components/Timer.svelte"; | ||||
| </script> | ||||
|  | ||||
| <main> | ||||
|   <label> | ||||
|     Fire after | ||||
|     <input type="number" bind:value={seconds} /> | ||||
|   </label> | ||||
|   <button on:click={start_timer}>Fire!</button> | ||||
|   <Timer /> | ||||
| </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 { | ||||
|   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,14 @@ 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; | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 from "./App.svelte"; | ||||
|  | ||||
|  | ||||
| const app = new App({ | ||||
|   target: document.getElementById("app"), | ||||
| }); | ||||
|  | ||||
| export default app; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,9 @@ | ||||
|     "module": "ESNext", | ||||
|     "resolveJsonModule": true, | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@app/*": ["./src/*"] | ||||
|     }, | ||||
|     /** | ||||
|      * Typecheck JS in `.svelte` and `.js` files by default. | ||||
|      * Disable checkJs if you'd like to use dynamic types in JS. | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| import { defineConfig } from "vite"; | ||||
| import { svelte } from "@sveltejs/vite-plugin-svelte"; | ||||
|  | ||||
|  | ||||
| // https://vitejs.dev/config/ | ||||
| export default defineConfig({ | ||||
|   plugins: [svelte()], | ||||
|   build: { | ||||
|     write: true, | ||||
|   }, | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       "@app": "./src/", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user