feat: add About dialog with version and license info

This commit is contained in:
2026-02-12 03:13:31 +09:00
parent ed13d00987
commit ac83c893eb
5 changed files with 182 additions and 9 deletions

View File

@@ -92,6 +92,7 @@ function resolveInvoke() {
const elements = {};
let tickInFlight = false;
let latestState = null;
let aboutInfo = null;
/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */
const SHORTCUTS = {
@@ -104,6 +105,7 @@ const SHORTCUTS = {
KeyL: () => requestState("add_lap"),
KeyP: () => requestState("reset_stopwatch"),
KeyH: () => toggleShortcutOverlay(),
KeyI: () => toggleAboutOverlay(),
};
window.addEventListener("DOMContentLoaded", () => {
@@ -141,6 +143,14 @@ function cacheElements() {
elements.shortcutOverlay = document.getElementById("shortcut-overlay");
elements.shortcutClose = document.getElementById("shortcut-close");
elements.shortcutTrigger = document.getElementById("shortcut-trigger");
elements.aboutOverlay = document.getElementById("about-overlay");
elements.aboutClose = document.getElementById("about-close");
elements.aboutTrigger = document.getElementById("about-trigger");
elements.aboutName = document.getElementById("about-name");
elements.aboutVersion = document.getElementById("about-version");
elements.aboutIdentifier = document.getElementById("about-identifier");
elements.aboutLicenseName = document.getElementById("about-license-name");
elements.aboutLicenseText = document.getElementById("about-license-text");
const missing = Object.entries(elements)
.filter(([, value]) => !value)
.map(([key]) => key);
@@ -198,6 +208,13 @@ function bindEvents() {
if (elements.shortcutTrigger) {
elements.shortcutTrigger.addEventListener("click", () => toggleShortcutOverlay(true));
}
elements.aboutClose.addEventListener("click", () => toggleAboutOverlay(false));
elements.aboutOverlay.addEventListener("click", (event) => {
if (event.target === elements.aboutOverlay) toggleAboutOverlay(false);
});
if (elements.aboutTrigger) {
elements.aboutTrigger.addEventListener("click", () => toggleAboutOverlay(true));
}
document.addEventListener("keydown", handleShortcuts, true);
logInfo("Core UI events bound");
@@ -209,11 +226,21 @@ function bindEvents() {
*/
async function bootstrap() {
logInfo("Bootstrapping state");
await requestState("get_state");
await Promise.all([requestState("get_state"), loadAboutInfo()]);
setInterval(runTick, 250);
logInfo("Tick loop scheduled", { intervalMs: 250 });
}
/** Loads application metadata used by the About dialog. */
async function loadAboutInfo() {
try {
aboutInfo = await invoke("get_about_info");
renderAboutInfo(aboutInfo);
} catch (error) {
logError("Failed to load About information", error);
}
}
/**
* Requests a timer toggle from the backend, optionally forcing a reparse of
* the duration input if the user explicitly pressed Enter inside the field.
@@ -401,6 +428,9 @@ function toggleShortcutOverlay(force) {
const overlay = elements.shortcutOverlay;
if (!overlay) return;
const next = typeof force === "boolean" ? force : overlay.hidden;
if (next) {
toggleAboutOverlay(false);
}
overlay.hidden = !next;
logInfo("Shortcut overlay toggled", { visible: next });
if (next) {
@@ -408,13 +438,46 @@ function toggleShortcutOverlay(force) {
}
}
/**
* Toggles the About dialog visibility and lazily refreshes metadata when
* missing so the UI can recover from early load failures.
*/
function toggleAboutOverlay(force) {
const overlay = elements.aboutOverlay;
if (!overlay) return;
const next = typeof force === "boolean" ? force : overlay.hidden;
if (next) {
toggleShortcutOverlay(false);
if (!aboutInfo) {
loadAboutInfo();
}
}
overlay.hidden = !next;
logInfo("About overlay toggled", { visible: next });
if (next) {
overlay.querySelector(".about-card")?.focus?.();
}
}
/** Applies About metadata fields into the dialog. */
function renderAboutInfo(info) {
if (!info) return;
elements.aboutName.textContent = info.app_name || "-";
elements.aboutVersion.textContent = info.version || "-";
elements.aboutIdentifier.textContent = info.identifier || "-";
elements.aboutLicenseName.textContent = info.license_name || "-";
elements.aboutLicenseText.textContent = info.license_text || "";
}
/**
* 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;
const shortcutOverlayVisible = elements.shortcutOverlay && !elements.shortcutOverlay.hidden;
const aboutOverlayVisible = elements.aboutOverlay && !elements.aboutOverlay.hidden;
const overlayVisible = shortcutOverlayVisible || aboutOverlayVisible;
const trackedKey = Boolean(SHORTCUTS[event.code]) || event.key === "Escape";
if (trackedKey) {
logDebug("Shortcut event received", {
@@ -425,11 +488,16 @@ function handleShortcuts(event) {
});
}
if (event.key === "Escape") {
if (overlayVisible) {
if (shortcutOverlayVisible) {
toggleShortcutOverlay(false);
event.preventDefault();
return;
}
if (aboutOverlayVisible) {
toggleAboutOverlay(false);
event.preventDefault();
return;
}
if (document.activeElement && document.activeElement !== document.body) {
document.activeElement.blur();
event.preventDefault();
@@ -437,7 +505,7 @@ function handleShortcuts(event) {
return;
}
if (event.altKey || event.metaKey || event.ctrlKey) return;
if (overlayVisible && event.code !== "KeyH") return;
if (overlayVisible && event.code !== "KeyH" && event.code !== "KeyI") return;
if (isTypingTarget(event.target)) return;
const handler = SHORTCUTS[event.code];
if (handler) {