Documentation
This commit is contained in:
82
src/main.js
82
src/main.js
@@ -1,7 +1,23 @@
|
||||
/**
|
||||
* Static prefix string used in every log entry to make browser console
|
||||
* filtering easier when multiple apps share the same window.
|
||||
*/
|
||||
const LOG_PREFIX = "[tauclock]";
|
||||
/**
|
||||
* Permitted log levels ordered by severity so we can determine whether a
|
||||
* message should be printed by comparing their indexes.
|
||||
*/
|
||||
const LOG_LEVELS = ["error", "warn", "info", "debug"];
|
||||
/**
|
||||
* Effective log level obtained either from localStorage, a query parameter or
|
||||
* the default, which then gates every log call in the frontend.
|
||||
*/
|
||||
const LOG_LEVEL = resolveLogLevel();
|
||||
|
||||
/**
|
||||
* JSDoc log wrapper that normalizes console usage and ensures we respect the
|
||||
* active log level before emitting potentially noisy diagnostic data.
|
||||
*/
|
||||
function logFrontend(level, message, details) {
|
||||
if (!shouldLog(level)) return;
|
||||
const output = `${LOG_PREFIX} ${message}`;
|
||||
@@ -18,6 +34,10 @@ const logWarn = (message, details) => logFrontend("warn", message, details);
|
||||
const logDebug = (message, details) => logFrontend("debug", message, details);
|
||||
const logError = (message, details) => logFrontend("error", message, details);
|
||||
|
||||
/**
|
||||
* Returns true if the requested log level is enabled by comparing the
|
||||
* requested level's index with the configured LOG_LEVEL bucket.
|
||||
*/
|
||||
function shouldLog(level) {
|
||||
const requested = LOG_LEVELS.indexOf(level);
|
||||
const active = LOG_LEVELS.indexOf(LOG_LEVEL);
|
||||
@@ -25,6 +45,10 @@ function shouldLog(level) {
|
||||
return requested <= active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the preferred log level by first checking localStorage, then the
|
||||
* `logLevel` URL parameter, and finally falling back to `info`.
|
||||
*/
|
||||
function resolveLogLevel() {
|
||||
const stored = window.localStorage?.getItem("tauclock:log-level");
|
||||
const normalizedStored = stored?.toLowerCase();
|
||||
@@ -39,6 +63,11 @@ function resolveLogLevel() {
|
||||
|
||||
const invoke = resolveInvoke();
|
||||
|
||||
/**
|
||||
* Finds the best available Tauri invoke bridge and emits structured debug
|
||||
* information for troubleshooting. If the bridge is missing, a stub is
|
||||
* returned so the UI can fail loudly rather than silently.
|
||||
*/
|
||||
function resolveInvoke() {
|
||||
const tauriGlobal = window.__TAURI__ || {};
|
||||
const candidates = [
|
||||
@@ -59,10 +88,12 @@ function resolveInvoke() {
|
||||
};
|
||||
}
|
||||
|
||||
/** Cache of DOM nodes keyed by logical role identifiers. */
|
||||
const elements = {};
|
||||
let tickInFlight = false;
|
||||
let latestState = null;
|
||||
|
||||
/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */
|
||||
const SHORTCUTS = {
|
||||
KeyA: () => focusAndSelect(elements.alarmTime),
|
||||
KeyN: () => elements.alarmForm.requestSubmit(),
|
||||
@@ -82,6 +113,10 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
bootstrap();
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds and stores all the DOM nodes we need to reference frequently so we
|
||||
* avoid repeated lookups every render.
|
||||
*/
|
||||
function cacheElements() {
|
||||
elements.localTime = document.getElementById("local-time");
|
||||
elements.localDate = document.getElementById("local-date");
|
||||
@@ -116,6 +151,10 @@ function cacheElements() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks up all DOM events, including keyboard shortcuts, button clicks, and
|
||||
* form submissions, translating them into backend commands where needed.
|
||||
*/
|
||||
function bindEvents() {
|
||||
elements.alarmForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -164,6 +203,10 @@ function bindEvents() {
|
||||
logInfo("Core UI events bound");
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the initial state fetch and schedules the recurring `tick` loop so
|
||||
* that the UI stays synchronized with the backend clock.
|
||||
*/
|
||||
async function bootstrap() {
|
||||
logInfo("Bootstrapping state");
|
||||
await requestState("get_state");
|
||||
@@ -171,6 +214,10 @@ async function bootstrap() {
|
||||
logInfo("Tick loop scheduled", { intervalMs: 250 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a timer toggle from the backend, optionally forcing a reparse of
|
||||
* the duration input if the user explicitly pressed Enter inside the field.
|
||||
*/
|
||||
function triggerTimerToggle(forceParse) {
|
||||
logDebug("Timer toggle requested", { forceParse, input: elements.timerInput.value });
|
||||
requestState("toggle_timer", {
|
||||
@@ -179,6 +226,10 @@ function triggerTimerToggle(forceParse) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic wrapper around the Tauri invoke API that logs both the request and
|
||||
* response, applies the new state snapshot, and gracefully handles failures.
|
||||
*/
|
||||
async function requestState(command, payload = {}) {
|
||||
try {
|
||||
logInfo(`Invoking backend command: ${command}`, payload);
|
||||
@@ -195,6 +246,11 @@ async function requestState(command, payload = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling task that asks the backend for a light-weight state update so that
|
||||
* timers, alarms, and stopwatches continue to advance even without user
|
||||
* interaction. Re-entrancy is avoided via `tickInFlight`.
|
||||
*/
|
||||
async function runTick() {
|
||||
if (tickInFlight) {
|
||||
logDebug("Tick skipped because a previous request is still pending");
|
||||
@@ -213,6 +269,10 @@ async function runTick() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a backend-provided snapshot to every UI subsystem (clock, alarms,
|
||||
* timer, stopwatch, and status messaging). Also plays the chime if requested.
|
||||
*/
|
||||
function applyState(state) {
|
||||
if (!state) return;
|
||||
latestState = state;
|
||||
@@ -231,6 +291,7 @@ function applyState(state) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-renders the clock labels for both local time and UTC. */
|
||||
function updateClock(clock) {
|
||||
if (!clock) return;
|
||||
elements.localTime.textContent = clock.local_time;
|
||||
@@ -239,6 +300,10 @@ function updateClock(clock) {
|
||||
elements.utcDate.textContent = clock.utc_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the alarm list including badges, actions, and empty states, while
|
||||
* also surfacing the next upcoming alarm summary.
|
||||
*/
|
||||
function renderAlarms(section) {
|
||||
const list = elements.alarmList;
|
||||
list.innerHTML = "";
|
||||
@@ -280,6 +345,7 @@ function renderAlarms(section) {
|
||||
elements.nextAlarm.textContent = section ? section.next_summary : "";
|
||||
}
|
||||
|
||||
/** Reflects the backend timer information in the UI controls and labels. */
|
||||
function renderTimer(view) {
|
||||
if (!view) return;
|
||||
elements.timerDisplay.textContent = view.display;
|
||||
@@ -287,6 +353,7 @@ function renderTimer(view) {
|
||||
elements.timerStatus.textContent = view.status_text || "";
|
||||
}
|
||||
|
||||
/** Displays stopwatch totals, the toggle label, and lap entries. */
|
||||
function renderStopwatch(view) {
|
||||
if (!view) return;
|
||||
elements.stopwatchDisplay.textContent = view.display;
|
||||
@@ -313,17 +380,23 @@ function renderStopwatch(view) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Shows transient status messages returned by the backend. */
|
||||
function renderStatus(message) {
|
||||
if (!elements.statusMessage) return;
|
||||
elements.statusMessage.textContent = message || "";
|
||||
}
|
||||
|
||||
/** Focuses a control and selects its text when possible for easy overwriting. */
|
||||
function focusAndSelect(element) {
|
||||
if (!element) return;
|
||||
element.focus();
|
||||
if (typeof element.select === "function") element.select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the keyboard shortcut cheat sheet overlay, optionally forcing the
|
||||
* visibility state when invoked with a boolean.
|
||||
*/
|
||||
function toggleShortcutOverlay(force) {
|
||||
const overlay = elements.shortcutOverlay;
|
||||
if (!overlay) return;
|
||||
@@ -335,6 +408,10 @@ function toggleShortcutOverlay(force) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global keyboard listener that translates single-key shortcuts into higher
|
||||
* level actions while respecting modifier keys, focus states, and overlays.
|
||||
*/
|
||||
function handleShortcuts(event) {
|
||||
if (event.defaultPrevented) return;
|
||||
const overlayVisible = elements.shortcutOverlay && !elements.shortcutOverlay.hidden;
|
||||
@@ -370,6 +447,7 @@ function handleShortcuts(event) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true when the event target is an editable/typing-friendly element. */
|
||||
function isTypingTarget(target) {
|
||||
if (!target) return false;
|
||||
const element = target instanceof Element ? target : target.parentElement;
|
||||
@@ -378,6 +456,10 @@ function isTypingTarget(target) {
|
||||
return Boolean(element.closest("input, textarea, select, [contenteditable='true']"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a very small synthesized chime using the Web Audio API in order to
|
||||
* alert the user even when no native notifications are available.
|
||||
*/
|
||||
function playChime(patternName = "alarm") {
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioCtx) return;
|
||||
|
||||
Reference in New Issue
Block a user