feat(audio player): canonical audiobook transports + dropdown settings + volume panel
Transport row is now the five-button audiobook shape the user asked for: previous-chapter · circular-skip-back (seconds displayed inside the ring) · play/pause · circular-skip-forward · next-chapter — the ring arrows are stroke SVGs (rotate-ccw/rotate-cw) with the number as an overlay so it stays upright and follows the skip setting. Settings strip collapses to four controls: Skip dropdown, Speed dropdown, Bookmark (capture popover unchanged), Volume. Volume opens a panel with a vertical dragger (writing-mode:rtl slider) and a mute/unmute button; the strip icon is dynamic — 1–3 arcs by level (low <34%, mid <67%, high) and crossed when muted or zero. Volume/mute persist in localStorage and restore with the element; unmute floors zero-volume to 50% so unmuting is audible. Verified on-device: dropdowns open upward with current value highlighted, mute flips both icons to the crossed state, slider drag changes arc count, chapter buttons jump (pill → 15/34) and the caption tracks the drawer's chapter labels.
This commit is contained in:
1 parent
86f0af632e
commit
5cf34102d6
3 files changed
+231
-32
No files matched your search
@@ -66,6 +66,17 @@ Alpine.data("audioReader", () => ({
|
||||
editingBookmarkId: "",
|
||||
editingNote: "",
|
||||
|
||||
// Settings strip dropdowns (open-state; panels close on outside click)
|
||||
skipOpen: false,
|
||||
speedOpen: false,
|
||||
volumeOpen: false,
|
||||
|
||||
// Volume: level 0..1 + mute, persisted separately. prevVolume restores
|
||||
// on unmute (spec: icon shows level arcs, crossed when muted).
|
||||
volume: Math.min(1, Math.max(0, Number(localStorage.getItem("audio_volume") ?? 1) || 1)),
|
||||
muted: localStorage.getItem("audio_muted") === "1",
|
||||
prevVolume: 1,
|
||||
|
||||
// chapters are absolute seconds into the book; the scrubber spans the
|
||||
// whole book, not per-chapter
|
||||
bookDuration: 0,
|
||||
@@ -106,6 +117,9 @@ Alpine.data("audioReader", () => ({
|
||||
audio.src = fileUrl;
|
||||
audio.playbackRate = Number(this.speed) || 1;
|
||||
audio.preload = "auto";
|
||||
// Restored volume/mute from the settings strip's persisted state.
|
||||
audio.volume = Math.min(1, Math.max(0, Number(this.volume)));
|
||||
audio.muted = !!this.muted;
|
||||
|
||||
// Restore position: chapters + progress from the server.
|
||||
this.restorePosition();
|
||||
@@ -299,6 +313,45 @@ Alpine.data("audioReader", () => ({
|
||||
window.location.href = `/media/${this.mediaItemId}`;
|
||||
},
|
||||
|
||||
// ---- volume (settings strip) ----
|
||||
|
||||
volumeIcon(): "muted" | "low" | "mid" | "high" {
|
||||
const v = this.muted ? 0 : Number(this.volume) || 0;
|
||||
if (v <= 0) return "muted";
|
||||
if (v < 0.34) return "low";
|
||||
if (v < 0.67) return "mid";
|
||||
return "high";
|
||||
},
|
||||
|
||||
setVolume(level: number) {
|
||||
const v = Math.min(1, Math.max(0, Number(level)));
|
||||
this.volume = v;
|
||||
localStorage.setItem("audio_volume", String(v));
|
||||
const audio = this.audio();
|
||||
if (audio) audio.volume = v;
|
||||
if (v > 0 && this.muted) this.toggleMute();
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
const audio = this.audio();
|
||||
if (this.muted) {
|
||||
// Unmute restores the last real level; a zero level gets a floor
|
||||
// so unmute is audible again.
|
||||
if (Number(this.volume) <= 0) {
|
||||
this.volume = 0.5;
|
||||
localStorage.setItem("audio_volume", "0.5");
|
||||
if (audio) audio.volume = 0.5;
|
||||
}
|
||||
this.muted = false;
|
||||
if (audio) audio.muted = false;
|
||||
} else {
|
||||
this.prevVolume = Number(this.volume) || 0.5;
|
||||
this.muted = true;
|
||||
if (audio) audio.muted = true;
|
||||
}
|
||||
localStorage.setItem("audio_muted", this.muted ? "1" : "0");
|
||||
},
|
||||
|
||||
// ---- bookmarks (A2d) ----
|
||||
|
||||
async loadBookmarks() {
|
||||
|
||||
Reference in new issue
Block a user