feat(audio-player): keyboard shortcuts — declarative keymap, dialog, media keys (plan §2)
Space/Enter (body-focus only), H/L + arrows skip, J/K + PgDn/PgUp chapter steps, B bookmark capture (note box auto-focuses, Enter saves even when blank, Shift+Enter inserts a newline, Esc cancels), Alt+arrows / Alt+H/L speed steps, up/down volume with a brief OSD, M mute, [ ] skip-duration cycle, ? shortcut dialog, Esc closes the top-most surface (help → capture → note edit → drawers → dropdowns). audio-keys.ts is the ONE declarative table: resolveShortcut() drives the document keydown dispatcher and visibleKeymap() renders the templ dialog (x-for over the exposed keymap — templ cannot import TS). Hardware media keys ride the existing MediaSession handlers; the Media* keydown codes resolve through the same table to the SAME methods, so both layers stay one behavior. Guards mirror the ebook reader: typing in any INPUT/TEXTAREA/contentEditable never triggers shortcuts (bookmark notes included), a focused button keeps Space/Enter for itself, and the focused scrubber keeps its arrows (its own @change seeks — a native arrow nudge moves 0.08s, not a 10s skip). SKIP_SECONDS consolidates to one const beside SPEEDS (the templ skip dropdown now iterates skipChoices; [ ] cycles via cycleSkip()) — the same one-source pattern as the §5 speed-list consolidation. Unit: audio-keys.test.ts (resolution, Alt disambiguation, media keys, keymap integrity and conflict-freedom, dialog visibility) and cycleSkip tests. Live: 39 Playwright checks on the KJV folder book covering every binding, the guards, dialog and OSD; probe user created and deleted.
This commit is contained in:
1 parent
db96ea91cd
commit
295c4b0664
6 files changed
+569
-22
No files matched your search
@@ -75,6 +75,19 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<path d="m6 9 6 6 6-6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="helpOpen = !helpOpen"
|
||||
class="rounded-full p-2 hover:bg-surface-hover transition-colors"
|
||||
style="color: var(--text-primary); border: 1px solid var(--border); background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);"
|
||||
aria-label="Keyboard shortcuts"
|
||||
title="Keyboard shortcuts (?)"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"></rect>
|
||||
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -219,7 +232,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7.5rem]"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;"
|
||||
>
|
||||
<template x-for="secs in [1, 5, 10, 15, 30]" :key="'skip' + secs">
|
||||
<template x-for="secs in skipChoices" :key="'skip' + secs">
|
||||
<button
|
||||
type="button"
|
||||
@click="setSkip(secs); skipOpen = false"
|
||||
@@ -453,10 +466,12 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
>
|
||||
<p class="text-sm font-semibold mb-2" style="color: var(--text-primary);">Bookmark at <span class="tabular-nums" x-text="fmtTime(currentTime)">0:00</span></p>
|
||||
<textarea
|
||||
id="bookmark-note"
|
||||
x-model="bookmarkNote"
|
||||
rows="3"
|
||||
placeholder="Optional note — markdown welcome (e.g. **great quote**, list, [link](https://…))"
|
||||
class="input mb-2 resize-y"
|
||||
@keydown.enter="if (!$event.shiftKey) { $event.preventDefault(); saveBookmark() }"
|
||||
></textarea>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" @click="bookmarkCaptureOpen = false" class="btn btn-ghost text-sm px-3 py-1.5">Cancel</button>
|
||||
@@ -464,6 +479,43 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Volume/mute OSD: brief confirmation for keyboard-driven changes -->
|
||||
<div
|
||||
x-show="osdVisible"
|
||||
x-cloak
|
||||
x-transition.opacity.duration.200ms
|
||||
class="fixed bottom-24 left-1/2 -translate-x-1/2 z-50 text-xs px-3 py-1.5 rounded-full shadow-lg tabular-nums"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||
x-text="osdText"
|
||||
></div>
|
||||
|
||||
<!-- Shortcut help (?): rows come from the shared KEYMAP
|
||||
(audio-keys.ts) — the same table drives the keydown handler. -->
|
||||
<div x-show="helpOpen" x-cloak class="fixed inset-0 z-40" style="background-color: color-mix(in srgb, black 32%, transparent);" @click="helpOpen = false"></div>
|
||||
<div x-show="helpOpen" x-cloak x-transition class="fixed inset-0 z-50 grid place-items-center p-4" @click.outside="helpOpen = false">
|
||||
<div class="w-full max-w-md rounded-2xl shadow-2xl p-4 max-h-[80vh] overflow-y-auto" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="flex items-center justify-between mb-3 px-1">
|
||||
<span class="text-sm font-semibold" style="color: var(--text-primary);">Keyboard shortcuts</span>
|
||||
<button type="button" @click="helpOpen = false" class="rounded-full p-1.5 hover:bg-surface-hover" style="color: var(--text-secondary);" aria-label="Close shortcuts">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<template x-for="spec in visibleKeymap()" :key="spec.action">
|
||||
<div class="flex items-center justify-between gap-3 px-2 py-1.5 rounded-lg">
|
||||
<span class="text-sm" style="color: var(--text-secondary);" x-text="spec.description"></span>
|
||||
<span class="flex items-center gap-1 shrink-0">
|
||||
<template x-for="k in spec.keys" :key="k.label">
|
||||
<kbd class="text-xs px-1.5 py-0.5 rounded tabular-nums" style="background-color: var(--surface-hover); color: var(--text-primary); border: 1px solid var(--border);" x-text="k.label"></kbd>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="audio-element" preload="auto" class="hidden"></audio>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user