Documentation

This commit is contained in:
2026-02-11 00:08:32 +09:00
parent 787d9c2e83
commit ed13d00987
3 changed files with 174 additions and 0 deletions

View File

@@ -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;