Compare commits
8 Commits
v0.1.0
...
5392214361
| Author | SHA1 | Date | |
|---|---|---|---|
| 5392214361 | |||
| a477a67936 | |||
| 7491866251 | |||
| b28502a199 | |||
| 53862d42a8 | |||
| d427dea70a | |||
| f39896374c | |||
| ac83c893eb |
16
.gitattributes
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
src-tauri/icons/128x128.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/128x128@2x.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square71x71Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/32x32.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/64x64.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square107x107Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square142x142Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square284x284Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square44x44Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square150x150Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/StoreLogo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/icon.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square89x89Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square30x30Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/Square310x310Logo.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
src-tauri/icons/app-icon.png filter=lfs diff=lfs merge=lfs -text
|
||||||
46
PKGBUILD
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
pkgname=tauclock-git
|
||||||
|
pkgver=0.1.0.r0.g0000000
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Keyboard-driven clock, alarm, timer, and stopwatch desktop app built with Tauri"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://git.rumginger.org/kamat/tauclock.git"
|
||||||
|
license=('MIT')
|
||||||
|
depends=(
|
||||||
|
'gtk3'
|
||||||
|
'webkit2gtk-4.1'
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
'cargo'
|
||||||
|
'git'
|
||||||
|
'rust'
|
||||||
|
)
|
||||||
|
source=("git+${url}")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "${srcdir}/tauclock/src-tauri"
|
||||||
|
local version
|
||||||
|
version="$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)"
|
||||||
|
printf '%s.r%s.g%s' "${version}" \
|
||||||
|
"$(git rev-list --count HEAD)" \
|
||||||
|
"$(git rev-parse --short HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "${srcdir}/tauclock/src-tauri"
|
||||||
|
export CARGO_TARGET_DIR="${srcdir}/target"
|
||||||
|
cargo build --release --locked
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "${srcdir}/tauclock"
|
||||||
|
|
||||||
|
install -Dm755 "${srcdir}/target/release/tauclock" \
|
||||||
|
"${pkgdir}/usr/bin/tauclock"
|
||||||
|
install -Dm644 "tauclock.desktop" \
|
||||||
|
"${pkgdir}/usr/share/applications/tauclock.desktop"
|
||||||
|
install -Dm644 "src-tauri/icons/128x128.png" \
|
||||||
|
"${pkgdir}/usr/share/icons/hicolor/128x128/apps/tauclock.png"
|
||||||
|
install -Dm644 "LICENSE" \
|
||||||
|
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ TauClockは、ローカル/UTC時計、アラーム、カウントダウンタ
|
|||||||
- 柔軟な入力形式に対応したカウントダウンタイマー(`5m30s`, `1:15`, `90` など)
|
- 柔軟な入力形式に対応したカウントダウンタイマー(`5m30s`, `1:15`, `90` など)
|
||||||
- ラップ記録つきストップウォッチと履歴リスト
|
- ラップ記録つきストップウォッチと履歴リスト
|
||||||
- キーボードショートカットと画面内に表示できるチートシート
|
- キーボードショートカットと画面内に表示できるチートシート
|
||||||
|
- アプリ説明・バージョン・ライセンス本文を確認できるAboutダイアログ
|
||||||
- Tauri + Rust製の軽量クロスプラットフォームバイナリ
|
- Tauri + Rust製の軽量クロスプラットフォームバイナリ
|
||||||
|
|
||||||
## キーボードショートカット
|
## キーボードショートカット
|
||||||
@@ -16,6 +17,7 @@ TauClockは、ローカル/UTC時計、アラーム、カウントダウンタ
|
|||||||
| キー | 動作 |
|
| キー | 動作 |
|
||||||
| ----- | ------------------------------------------ |
|
| ----- | ------------------------------------------ |
|
||||||
| `H` | ショートカット一覧を開閉 |
|
| `H` | ショートカット一覧を開閉 |
|
||||||
|
| `I` | Aboutダイアログを開閉 |
|
||||||
| `A` | アラームの時刻入力欄にフォーカス |
|
| `A` | アラームの時刻入力欄にフォーカス |
|
||||||
| `N` | アラームを追加 |
|
| `N` | アラームを追加 |
|
||||||
| `T` | タイマーの時間入力欄にフォーカス |
|
| `T` | タイマーの時間入力欄にフォーカス |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 129 B |
BIN
src-tauri/icons/64x64.png
LFS
Normal file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 129 B |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 99 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src-tauri/icons/app-icon.png
LFS
Normal file
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 131 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -5,10 +5,21 @@
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
use serde::Serialize;
|
||||||
use state::{AppState, FrontendState};
|
use state::{AppState, FrontendState};
|
||||||
use std::{path::PathBuf, str::FromStr};
|
use std::{path::PathBuf, str::FromStr};
|
||||||
use tauri_plugin_log::{Target, TargetKind};
|
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
|
/// Returns the current frontend snapshot without mutating any state. Used for
|
||||||
/// the initial UI bootstrap.
|
/// the initial UI bootstrap.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -107,6 +118,21 @@ fn reset_stopwatch(app_state: tauri::State<AppState>) -> FrontendState {
|
|||||||
app_state.reset_stopwatch()
|
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
|
/// Initializes the Tauri runtime, configures plugins, and wires shared
|
||||||
/// application state into every window.
|
/// application state into every window.
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -139,7 +165,8 @@ pub fn run() {
|
|||||||
reset_timer,
|
reset_timer,
|
||||||
toggle_stopwatch,
|
toggle_stopwatch,
|
||||||
add_lap,
|
add_lap,
|
||||||
reset_stopwatch
|
reset_stopwatch,
|
||||||
|
get_about_info
|
||||||
])
|
])
|
||||||
.setup(move |_| {
|
.setup(move |_| {
|
||||||
log::info!("Tauclock backend ready; storage_path={}", storage_display);
|
log::info!("Tauclock backend ready; storage_path={}", storage_display);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-meta">
|
<div class="header-meta">
|
||||||
<button type="button" id="shortcut-trigger" class="hint-button">ショートカット一覧 (H)</button>
|
<button type="button" id="shortcut-trigger" class="hint-button">ショートカット一覧 (H)</button>
|
||||||
|
<button type="button" id="about-trigger" class="hint-button">About (I)</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -118,6 +119,8 @@
|
|||||||
<dl>
|
<dl>
|
||||||
<dt>H</dt>
|
<dt>H</dt>
|
||||||
<dd>ショートカット一覧を表示/非表示</dd>
|
<dd>ショートカット一覧を表示/非表示</dd>
|
||||||
|
<dt>I</dt>
|
||||||
|
<dd>Aboutを表示/非表示</dd>
|
||||||
<dt>A</dt>
|
<dt>A</dt>
|
||||||
<dd>アラーム時刻入力へフォーカス</dd>
|
<dd>アラーム時刻入力へフォーカス</dd>
|
||||||
<dt>N</dt>
|
<dt>N</dt>
|
||||||
@@ -139,5 +142,26 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="about-overlay" class="about-overlay" hidden>
|
||||||
|
<div class="about-card" role="dialog" aria-modal="true" aria-labelledby="about-heading" tabindex="-1">
|
||||||
|
<header>
|
||||||
|
<h2 id="about-heading">About TauClock</h2>
|
||||||
|
<button type="button" id="about-close" class="ghost">閉じる (Esc)</button>
|
||||||
|
</header>
|
||||||
|
<dl class="about-meta">
|
||||||
|
<dt>アプリ名</dt>
|
||||||
|
<dd id="about-name">-</dd>
|
||||||
|
<dt>バージョン</dt>
|
||||||
|
<dd id="about-version">-</dd>
|
||||||
|
<dt>識別子</dt>
|
||||||
|
<dd id="about-identifier">-</dd>
|
||||||
|
<dt>ライセンス</dt>
|
||||||
|
<dd id="about-license-name">-</dd>
|
||||||
|
</dl>
|
||||||
|
<h3>ライセンス本文</h3>
|
||||||
|
<pre id="about-license-text" class="about-license"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
76
src/main.js
@@ -92,6 +92,7 @@ function resolveInvoke() {
|
|||||||
const elements = {};
|
const elements = {};
|
||||||
let tickInFlight = false;
|
let tickInFlight = false;
|
||||||
let latestState = null;
|
let latestState = null;
|
||||||
|
let aboutInfo = null;
|
||||||
|
|
||||||
/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */
|
/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */
|
||||||
const SHORTCUTS = {
|
const SHORTCUTS = {
|
||||||
@@ -104,6 +105,7 @@ const SHORTCUTS = {
|
|||||||
KeyL: () => requestState("add_lap"),
|
KeyL: () => requestState("add_lap"),
|
||||||
KeyP: () => requestState("reset_stopwatch"),
|
KeyP: () => requestState("reset_stopwatch"),
|
||||||
KeyH: () => toggleShortcutOverlay(),
|
KeyH: () => toggleShortcutOverlay(),
|
||||||
|
KeyI: () => toggleAboutOverlay(),
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
@@ -141,6 +143,14 @@ function cacheElements() {
|
|||||||
elements.shortcutOverlay = document.getElementById("shortcut-overlay");
|
elements.shortcutOverlay = document.getElementById("shortcut-overlay");
|
||||||
elements.shortcutClose = document.getElementById("shortcut-close");
|
elements.shortcutClose = document.getElementById("shortcut-close");
|
||||||
elements.shortcutTrigger = document.getElementById("shortcut-trigger");
|
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)
|
const missing = Object.entries(elements)
|
||||||
.filter(([, value]) => !value)
|
.filter(([, value]) => !value)
|
||||||
.map(([key]) => key);
|
.map(([key]) => key);
|
||||||
@@ -198,6 +208,13 @@ function bindEvents() {
|
|||||||
if (elements.shortcutTrigger) {
|
if (elements.shortcutTrigger) {
|
||||||
elements.shortcutTrigger.addEventListener("click", () => toggleShortcutOverlay(true));
|
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);
|
document.addEventListener("keydown", handleShortcuts, true);
|
||||||
logInfo("Core UI events bound");
|
logInfo("Core UI events bound");
|
||||||
@@ -209,11 +226,21 @@ function bindEvents() {
|
|||||||
*/
|
*/
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
logInfo("Bootstrapping state");
|
logInfo("Bootstrapping state");
|
||||||
await requestState("get_state");
|
await Promise.all([requestState("get_state"), loadAboutInfo()]);
|
||||||
setInterval(runTick, 250);
|
setInterval(runTick, 250);
|
||||||
logInfo("Tick loop scheduled", { intervalMs: 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
|
* Requests a timer toggle from the backend, optionally forcing a reparse of
|
||||||
* the duration input if the user explicitly pressed Enter inside the field.
|
* the duration input if the user explicitly pressed Enter inside the field.
|
||||||
@@ -401,6 +428,9 @@ function toggleShortcutOverlay(force) {
|
|||||||
const overlay = elements.shortcutOverlay;
|
const overlay = elements.shortcutOverlay;
|
||||||
if (!overlay) return;
|
if (!overlay) return;
|
||||||
const next = typeof force === "boolean" ? force : overlay.hidden;
|
const next = typeof force === "boolean" ? force : overlay.hidden;
|
||||||
|
if (next) {
|
||||||
|
toggleAboutOverlay(false);
|
||||||
|
}
|
||||||
overlay.hidden = !next;
|
overlay.hidden = !next;
|
||||||
logInfo("Shortcut overlay toggled", { visible: next });
|
logInfo("Shortcut overlay toggled", { visible: next });
|
||||||
if (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
|
* Global keyboard listener that translates single-key shortcuts into higher
|
||||||
* level actions while respecting modifier keys, focus states, and overlays.
|
* level actions while respecting modifier keys, focus states, and overlays.
|
||||||
*/
|
*/
|
||||||
function handleShortcuts(event) {
|
function handleShortcuts(event) {
|
||||||
if (event.defaultPrevented) return;
|
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";
|
const trackedKey = Boolean(SHORTCUTS[event.code]) || event.key === "Escape";
|
||||||
if (trackedKey) {
|
if (trackedKey) {
|
||||||
logDebug("Shortcut event received", {
|
logDebug("Shortcut event received", {
|
||||||
@@ -425,11 +488,16 @@ function handleShortcuts(event) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
if (overlayVisible) {
|
if (shortcutOverlayVisible) {
|
||||||
toggleShortcutOverlay(false);
|
toggleShortcutOverlay(false);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (aboutOverlayVisible) {
|
||||||
|
toggleAboutOverlay(false);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (document.activeElement && document.activeElement !== document.body) {
|
if (document.activeElement && document.activeElement !== document.body) {
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -437,7 +505,7 @@ function handleShortcuts(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.altKey || event.metaKey || event.ctrlKey) 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;
|
if (isTypingTarget(event.target)) return;
|
||||||
const handler = SHORTCUTS[event.code];
|
const handler = SHORTCUTS[event.code];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ h1 {
|
|||||||
.header-meta {
|
.header-meta {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #b6c5ff;
|
color: #b6c5ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
@@ -396,8 +399,9 @@ button.ghost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keyboard shortcut modal overlay */
|
/* Keyboard shortcut/About modal overlay */
|
||||||
.shortcut-overlay {
|
.shortcut-overlay,
|
||||||
|
.about-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
@@ -407,7 +411,8 @@ button.ghost {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-card {
|
.shortcut-card,
|
||||||
|
.about-card {
|
||||||
width: min(520px, 80vw);
|
width: min(520px, 80vw);
|
||||||
background: rgba(4, 8, 24, 0.92);
|
background: rgba(4, 8, 24, 0.92);
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
@@ -416,7 +421,8 @@ button.ghost {
|
|||||||
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.55);
|
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-card header {
|
.shortcut-card header,
|
||||||
|
.about-card header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -441,6 +447,48 @@ button.ghost {
|
|||||||
color: #e5ebff;
|
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 */
|
/* Responsive adjustments for tablet/phone layouts */
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
@@ -482,4 +530,8 @@ button.ghost {
|
|||||||
.shortcut-card dl {
|
.shortcut-card dl {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.about-meta {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
tauclock.desktop
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=TauClock
|
||||||
|
Comment=Keyboard-driven clock, alarm, timer, and stopwatch
|
||||||
|
Exec=tauclock
|
||||||
|
Icon=tauclock
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Clock;
|
||||||
|
StartupNotify=true
|
||||||