From ac83c893eb031e0bf7fec598c912348f2cf7d09e Mon Sep 17 00:00:00 2001 From: Yuya KAMATAKI Date: Thu, 12 Feb 2026 03:13:31 +0900 Subject: [PATCH] feat: add About dialog with version and license info --- README.md | 2 ++ src-tauri/src/lib.rs | 29 ++++++++++++++++- src/index.html | 24 ++++++++++++++ src/main.js | 76 +++++++++++++++++++++++++++++++++++++++++--- src/styles.css | 60 +++++++++++++++++++++++++++++++--- 5 files changed, 182 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 02feb84..d16f5d5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ TauClockは、ローカル/UTC時計、アラーム、カウントダウンタ - 柔軟な入力形式に対応したカウントダウンタイマー(`5m30s`, `1:15`, `90` など) - ラップ記録つきストップウォッチと履歴リスト - キーボードショートカットと画面内に表示できるチートシート +- アプリ説明・バージョン・ライセンス本文を確認できるAboutダイアログ - Tauri + Rust製の軽量クロスプラットフォームバイナリ ## キーボードショートカット @@ -16,6 +17,7 @@ TauClockは、ローカル/UTC時計、アラーム、カウントダウンタ | キー | 動作 | | ----- | ------------------------------------------ | | `H` | ショートカット一覧を開閉 | +| `I` | Aboutダイアログを開閉 | | `A` | アラームの時刻入力欄にフォーカス | | `N` | アラームを追加 | | `T` | タイマーの時間入力欄にフォーカス | diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cc51e57..57ffa29 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,10 +5,21 @@ mod state; use log::LevelFilter; +use serde::Serialize; use state::{AppState, FrontendState}; use std::{path::PathBuf, str::FromStr}; use tauri_plugin_log::{Target, TargetKind}; +/// Metadata and legal text displayed by the frontend "About" dialog. +#[derive(Debug, Clone, Serialize)] +struct AboutInfo { + app_name: String, + version: String, + identifier: String, + license_name: String, + license_text: String, +} + /// Returns the current frontend snapshot without mutating any state. Used for /// the initial UI bootstrap. #[tauri::command] @@ -107,6 +118,21 @@ fn reset_stopwatch(app_state: tauri::State) -> FrontendState { app_state.reset_stopwatch() } +/// Returns static application metadata and the bundled MIT license text for +/// display in the About dialog. +#[tauri::command] +fn get_about_info(app: tauri::AppHandle) -> AboutInfo { + let package = app.package_info(); + let identifier = app.config().identifier.clone(); + AboutInfo { + app_name: package.name.clone(), + version: package.version.to_string(), + identifier, + license_name: "MIT".to_string(), + license_text: include_str!("../../LICENSE").trim().to_string(), + } +} + /// Initializes the Tauri runtime, configures plugins, and wires shared /// application state into every window. #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -139,7 +165,8 @@ pub fn run() { reset_timer, toggle_stopwatch, add_lap, - reset_stopwatch + reset_stopwatch, + get_about_info ]) .setup(move |_| { log::info!("Tauclock backend ready; storage_path={}", storage_display); diff --git a/src/index.html b/src/index.html index 4da9f14..9316c02 100644 --- a/src/index.html +++ b/src/index.html @@ -24,6 +24,7 @@
+
@@ -118,6 +119,8 @@
H
ショートカット一覧を表示/非表示
+
I
+
Aboutを表示/非表示
A
アラーム時刻入力へフォーカス
N
@@ -139,5 +142,26 @@
+ + diff --git a/src/main.js b/src/main.js index b25d534..d409886 100644 --- a/src/main.js +++ b/src/main.js @@ -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) { diff --git a/src/styles.css b/src/styles.css index abcc805..11cbf77 100644 --- a/src/styles.css +++ b/src/styles.css @@ -77,6 +77,9 @@ h1 { .header-meta { font-size: 0.85rem; color: #b6c5ff; + display: flex; + align-items: center; + gap: 0.6rem; } .hint { @@ -396,8 +399,9 @@ button.ghost { } } -/* Keyboard shortcut modal overlay */ -.shortcut-overlay { +/* Keyboard shortcut/About modal overlay */ +.shortcut-overlay, +.about-overlay { position: fixed; inset: 0; backdrop-filter: blur(8px); @@ -407,7 +411,8 @@ button.ghost { justify-content: center; } -.shortcut-card { +.shortcut-card, +.about-card { width: min(520px, 80vw); background: rgba(4, 8, 24, 0.92); border-radius: 1.5rem; @@ -416,7 +421,8 @@ button.ghost { box-shadow: 0 25px 70px rgba(0, 0, 0, 0.55); } -.shortcut-card header { +.shortcut-card header, +.about-card header { display: flex; justify-content: space-between; align-items: center; @@ -441,6 +447,48 @@ button.ghost { color: #e5ebff; } +.about-card { + width: min(760px, 92vw); + max-height: min(84vh, 780px); + overflow: auto; +} + +.about-meta { + margin: 1rem 0; + display: grid; + grid-template-columns: minmax(120px, 180px) 1fr; + gap: 0.3rem 0.75rem; +} + +.about-meta dt { + color: #9dc0ff; + font-weight: 600; +} + +.about-meta dd { + margin: 0; + color: #e5ebff; + overflow-wrap: anywhere; +} + +.about-card h3 { + margin: 0.6rem 0 0.45rem; + font-size: 0.95rem; + color: #9dc0ff; +} + +.about-license { + margin: 0; + padding: 1rem; + border-radius: 1rem; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(1, 3, 11, 0.8); + color: #d9e5ff; + line-height: 1.45; + white-space: pre-wrap; + font-size: 0.84rem; +} + /* Responsive adjustments for tablet/phone layouts */ @media (max-width: 1180px) { .app-shell { @@ -482,4 +530,8 @@ button.ghost { .shortcut-card dl { grid-template-columns: 1fr; } + + .about-meta { + grid-template-columns: 1fr; + } }