[refactor] copied the existing types into the Svelte App
This commit is contained in:
79
src/app.css
79
src/app.css
@@ -1,79 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
3
src/app/app.css
Normal file
3
src/app/app.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "@picocss/pico/css/pico.classless.jade.min.css";
|
||||
@import "./styles/base.css";
|
||||
@import "./styles/countdown.css";
|
||||
16
src/app/styles/base.css
Normal file
16
src/app/styles/base.css
Normal file
@@ -0,0 +1,16 @@
|
||||
:root {
|
||||
--pico-border-radius: 0.5rem;
|
||||
--pico-typography-spacing-vertical: 1.5rem;
|
||||
--pico-form-element-spacing-vertical: 1rem;
|
||||
--pico-form-element-spacing-horizontal: 1.25rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
--pico-font-family: Pacifico, cursive;
|
||||
--pico-font-weight: 400;
|
||||
--pico-typography-spacing-vertical: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
--pico-font-weight: 700;
|
||||
}
|
||||
10
src/app/styles/countdown.css
Normal file
10
src/app/styles/countdown.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.countdown-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.countdown-error {
|
||||
min-height: 1.25rem;
|
||||
color: #b00020;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
45
src/features/countdown/api.ts
Normal file
45
src/features/countdown/api.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {invokeCommand} from "../../shared/tauri/invoke";
|
||||
import {CountdownCommand, CountdownSnapshot, CountdownState, Duration} from "./types";
|
||||
import {millisToDuration} from "./helper.ts";
|
||||
|
||||
type CountdownSnapshotDto = {
|
||||
id: number;
|
||||
label: string;
|
||||
duration: number;
|
||||
state: CountdownState;
|
||||
start_epoch_ms: number | null;
|
||||
target_epoch_ms: number | null;
|
||||
};
|
||||
|
||||
export async function fetchCountdownSnapshot(): Promise<CountdownSnapshot> {
|
||||
let temp = await invokeCommand<CountdownSnapshotDto>("countdown_snapshot");
|
||||
const duration: Duration = millisToDuration(temp.duration);
|
||||
return {
|
||||
id: temp.id,
|
||||
label: temp.label,
|
||||
duration: duration,
|
||||
state: temp.state,
|
||||
start_epoch: temp.start_epoch_ms !== null ? new Date(temp.start_epoch_ms) : null,
|
||||
target_epoch: temp.target_epoch_ms !== null ? new Date(temp.target_epoch_ms) : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function invokeCountdownCommand(command: CountdownCommand): Promise<void> {
|
||||
await invokeCommand<void>(command);
|
||||
}
|
||||
|
||||
export async function startCountdown(): Promise<void> {
|
||||
await invokeCountdownCommand("countdown_start");
|
||||
}
|
||||
|
||||
export async function pauseCountdown(): Promise<void> {
|
||||
await invokeCountdownCommand("countdown_pause");
|
||||
}
|
||||
|
||||
export async function resumeCountdown(): Promise<void> {
|
||||
await invokeCountdownCommand("countdown_resume");
|
||||
}
|
||||
|
||||
export async function resetCountdown(): Promise<void> {
|
||||
await invokeCountdownCommand("countdown_reset");
|
||||
}
|
||||
48
src/features/countdown/controller.ts
Normal file
48
src/features/countdown/controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
fetchCountdownSnapshot,
|
||||
pauseCountdown,
|
||||
resetCountdown,
|
||||
resumeCountdown,
|
||||
startCountdown,
|
||||
} from "./api";
|
||||
import { createCountdownView } from "./view";
|
||||
|
||||
export async function initCountdownController(container: HTMLElement): Promise<void> {
|
||||
const view = createCountdownView(container);
|
||||
|
||||
const refreshSnapshot = async (): Promise<void> => {
|
||||
try {
|
||||
const snapshot = await fetchCountdownSnapshot();
|
||||
view.setSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
view.setError(`snapshot error: ${String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const runAction = async (action: () => Promise<void>): Promise<void> => {
|
||||
try {
|
||||
await action();
|
||||
await refreshSnapshot();
|
||||
} catch (error) {
|
||||
view.setError(`command error: ${String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
view.onStart(() => {
|
||||
void runAction(startCountdown);
|
||||
});
|
||||
view.onPause(() => {
|
||||
void runAction(pauseCountdown);
|
||||
});
|
||||
view.onResume(() => {
|
||||
void runAction(resumeCountdown);
|
||||
});
|
||||
view.onReset(() => {
|
||||
void runAction(resetCountdown);
|
||||
});
|
||||
view.onRefresh(() => {
|
||||
void refreshSnapshot();
|
||||
});
|
||||
|
||||
await refreshSnapshot();
|
||||
}
|
||||
18
src/features/countdown/types.ts
Normal file
18
src/features/countdown/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type {Duration} from "../../shared/time/duration";
|
||||
|
||||
export type CountdownState = "Idle" | "Running" | "Paused" | "Finished";
|
||||
|
||||
export type CountdownSnapshot = {
|
||||
id: number;
|
||||
label: string;
|
||||
duration: Duration;
|
||||
state: CountdownState;
|
||||
start_epoch: Date | null;
|
||||
target_epoch: Date | null;
|
||||
}
|
||||
|
||||
export type CountdownCommand =
|
||||
| "countdown_start"
|
||||
| "countdown_pause"
|
||||
| "countdown_resume"
|
||||
| "countdown_reset";
|
||||
79
src/features/countdown/view.ts
Normal file
79
src/features/countdown/view.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {formatDuration} from "./helper.ts";
|
||||
import {CountdownSnapshot} from "./types.ts";
|
||||
|
||||
export type CountdownView = {
|
||||
onStart: (handler: () => void) => void;
|
||||
onResume: (handler: () => void) => void;
|
||||
onPause: (handler: () => void) => void;
|
||||
onReset: (handler: () => void) => void;
|
||||
onRefresh: (handler: () => void) => void;
|
||||
setSnapshot: (snapshot: CountdownSnapshot) => void;
|
||||
setError: (message: string) => void;
|
||||
};
|
||||
|
||||
function makeButton(label: string): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.id = "cd-btn-" + label.toLowerCase();
|
||||
button.className = "countdown-btn"
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
return button;
|
||||
}
|
||||
|
||||
export function createCountdownView(container: HTMLElement): CountdownView {
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "countdown-panel";
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "Countdown Controls";
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "countdown-actions";
|
||||
|
||||
const startButton = makeButton("Start");
|
||||
const resumeButton = makeButton("Resume");
|
||||
const pauseButton = makeButton("Pause");
|
||||
const resetButton = makeButton("Reset");
|
||||
const refreshButton = makeButton("Refresh");
|
||||
|
||||
actions.append(startButton, resumeButton, pauseButton, resetButton, refreshButton);
|
||||
|
||||
const summary = document.createElement("p");
|
||||
summary.className = "countdown-summary";
|
||||
summary.textContent = "Waiting for snapshot...";
|
||||
|
||||
const error = document.createElement("p");
|
||||
error.className = "countdown-error";
|
||||
|
||||
const snapshot = document.createElement("pre");
|
||||
snapshot.className = "countdown-snapshot";
|
||||
|
||||
panel.append(title, actions, summary, error, snapshot);
|
||||
container.appendChild(panel);
|
||||
|
||||
return {
|
||||
onStart(handler) {
|
||||
startButton.addEventListener("click", handler);
|
||||
},
|
||||
onResume(handler) {
|
||||
resumeButton.addEventListener("click", handler);
|
||||
},
|
||||
onPause(handler) {
|
||||
pauseButton.addEventListener("click", handler);
|
||||
},
|
||||
onReset(handler) {
|
||||
resetButton.addEventListener("click", handler);
|
||||
},
|
||||
onRefresh(handler) {
|
||||
refreshButton.addEventListener("click", handler);
|
||||
},
|
||||
setSnapshot(value) {
|
||||
error.textContent = "";
|
||||
summary.textContent = `State: ${value.state} | Remaining: ${formatDuration(value.duration)}`;
|
||||
snapshot.textContent = JSON.stringify(value, null, 2);
|
||||
},
|
||||
setError(message) {
|
||||
error.textContent = message;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import {mount} from 'svelte'
|
||||
import './app/app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
8
src/shared/tauri/invoke.ts
Normal file
8
src/shared/tauri/invoke.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export async function invokeCommand<T>(
|
||||
command: string,
|
||||
payload?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
return invoke<T>(command, payload);
|
||||
}
|
||||
19
src/shared/time/duration.ts
Normal file
19
src/shared/time/duration.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type Duration = {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
millis: number;
|
||||
}
|
||||
|
||||
export function formatDuration(duration: Duration): string {
|
||||
return `${duration.hours.toString().padStart(2, '0')}:${duration.minutes.toString().padStart(2, '0')}:${duration.seconds.toString().padStart(2, '0')}.${duration.millis.toString().padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
export function millisToDuration(millis: number): Duration {
|
||||
return {
|
||||
hours: Math.floor(millis / 3600000),
|
||||
minutes: Math.floor((millis % 3600000) / 60000),
|
||||
seconds: Math.floor((millis % 60000) / 1000),
|
||||
millis: millis % 1000,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user