feat(audio-player): sleep timer — moon pill, countdown, 5s fade then pause (plan §3)
Off / 5 / 10 / 15 / 30 / 45 / 60 minutes / end-of-chapter. The moon pill sits in the settings strip between Bookmark and Volume and shows the live mm:ss countdown while armed (or 'End of chapter'); the dropdown's armed row highlights like Skip/Speed. audio-sleep.ts is the pure, DOM-free state machine (arm/due/fired/ fade-ramp/label + the SLEEP_CHOICES vocabulary the templ dropdown iterates). The component owns the timers: a 1s countdown ticker in minutes mode, and a ~5s element-volume fade on expiry that ends in audio.pause() — the SAME pause listener a manual pause fires, so the progress push and media-session state land. The user's configured volume is restored right after the pause, and the fade operates on the element's volume the user controls, not a parallel path. End-of-chapter fires when the armed chapter is left — natural crossing (tickChapter) or a manual jump (jumpToChapter) alike. Manual play during a fade aborts the expiry (you're awake, you asked); stopSleep restores any partial fade. Per-session ONLY: nothing touches localStorage — the reload check proves it starts Off. sleepOpen joins closeTopSurface's dropdown branch, so Esc closes it with the other settings-strip surfaces. Muted (or zero-volume) expiry skips the pantomime and pauses immediately. Unit: audio-sleep.test.ts (deadline mechanics, chapter-fired semantics both directions, fade ramp clamps, label formatting, SLEEP_CHOICES integrity). Live: 24 Playwright checks on the KJV book — dropdown, countdown ticking, armed-row highlight, end-of-chapter fire + reset, internal 3s deadline fade→pause→restore, play-abort, reload-starts-Off; probe user created and deleted.
This commit is contained in:
1 parent
295c4b0664
commit
fe7eab91e8
5 files changed
+369
-2
No files matched your search
@@ -288,6 +288,38 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
</svg>
|
||||
<span>Bookmark</span>
|
||||
</button>
|
||||
<div class="relative" @click.outside="sleepOpen = false">
|
||||
<button
|
||||
type="button"
|
||||
@click="sleepOpen = !sleepOpen"
|
||||
class="text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5"
|
||||
:aria-label="'Sleep timer, currently ' + sleepLabelStr"
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>
|
||||
<span class="font-semibold tabular-nums" style="color: var(--text-primary);" x-text="sleepLabelStr === 'Off' ? 'Sleep' : sleepLabelStr">Sleep</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="sleepOpen"
|
||||
x-cloak
|
||||
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-[8.5rem]"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;"
|
||||
>
|
||||
<template x-for="opt in sleepChoices" :key="opt.key">
|
||||
<button
|
||||
type="button"
|
||||
@click="chooseSleep(opt); sleepOpen = false"
|
||||
class="w-full text-left px-3 py-1.5 rounded-md text-sm"
|
||||
:class="isSleepActive(opt.key) ? 'font-semibold' : ''"
|
||||
:style="isSleepActive(opt.key) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
|
||||
x-text="opt.label"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" @click.outside="volumeOpen = false">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user