feat: add chapter announcing setting to audio player
All checks were successful
Release / Test backend (push) Successful in 43s
Release / Check ui (push) Successful in 1m51s
Release / Docker / caddy (push) Successful in 41s
Release / Docker / backend (push) Successful in 2m24s
Release / Docker / runner (push) Successful in 2m49s
Release / Upload source maps (push) Successful in 1m25s
Release / Docker / ui (push) Successful in 2m12s
Release / Gitea Release (push) Successful in 30s

When enabled, the Web Speech API speaks the upcoming chapter number and
title (e.g. 'Chapter 12 — The Final Battle') between auto-next chapters,
giving an audible cue before the next narration begins.

- AudioStore.announceChapter ( boolean, default false)
- PBUserSettings.announce_chapter persisted to PocketBase
- GET/PUT /api/settings includes announceChapter field
- +layout.server.ts loads + defaults the field
- +layout.svelte applies on load, saves in debounced PUT, and fires
  SpeechSynthesisUtterance in onended before navigating (falls back to
  immediate navigation if speechSynthesis is unavailable)
- ListeningMode: 'Announce' pill added to the Speed · Auto · Sleep row
This commit is contained in:
root
2026-04-06 21:12:10 +05:00
parent 93e9d88066
commit 1f83a7c05f
6 changed files with 66 additions and 12 deletions

View File

@@ -101,6 +101,13 @@ class AudioStore {
*/
autoNext = $state(false);
/**
* When true, announces the upcoming chapter number and title via the
* Web Speech API before auto-next navigation fires.
* e.g. "Chapter 12 — The Final Battle"
*/
announceChapter = $state(false);
/**
* The next chapter number for the currently playing chapter, or null if
* there is no next chapter. Written by the chapter page's AudioPlayer.

View File

@@ -618,8 +618,8 @@
{/if}
</div>
<!-- Secondary controls: unified single row — Speed · Auto · Sleep -->
<div class="flex items-center justify-center gap-2 shrink-0">
<!-- Secondary controls: unified single row — Speed · Auto · Announce · Sleep -->
<div class="flex items-center justify-center gap-2 shrink-0 flex-wrap">
<!-- Speed — segmented pill -->
<div class="flex items-center gap-0.5 bg-(--color-surface-2) rounded-full px-1.5 py-1 border border-(--color-border)">
{#each SPEED_OPTIONS as s}
@@ -661,6 +661,25 @@
{/if}
</button>
<!-- Announce chapter pill (only meaningful when auto-next is on) -->
<button
type="button"
onclick={() => (audioStore.announceChapter = !audioStore.announceChapter)}
class={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors',
audioStore.announceChapter
? 'border-(--color-brand) bg-(--color-brand)/15 text-(--color-brand)'
: 'border-(--color-border) bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text)'
)}
aria-pressed={audioStore.announceChapter}
title={audioStore.announceChapter ? 'Chapter announcing on' : 'Chapter announcing off'}
>
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
Announce
</button>
<!-- Sleep timer pill -->
<button
type="button"

View File

@@ -75,6 +75,7 @@ export interface PBUserSettings {
locale?: string;
font_family?: string;
font_size?: number;
announce_chapter?: boolean;
updated?: string;
}
@@ -998,7 +999,7 @@ export async function getSettings(
export async function saveSettings(
sessionId: string,
settings: { autoNext: boolean; voice: string; speed: number; theme?: string; locale?: string; fontFamily?: string; fontSize?: number },
settings: { autoNext: boolean; voice: string; speed: number; theme?: string; locale?: string; fontFamily?: string; fontSize?: number; announceChapter?: boolean },
userId?: string
): Promise<void> {
const existing = await listOne<PBUserSettings & { id: string }>(
@@ -1017,6 +1018,7 @@ export async function saveSettings(
if (settings.locale !== undefined) payload.locale = settings.locale;
if (settings.fontFamily !== undefined) payload.font_family = settings.fontFamily;
if (settings.fontSize !== undefined) payload.font_size = settings.fontSize;
if (settings.announceChapter !== undefined) payload.announce_chapter = settings.announceChapter;
if (userId) payload.user_id = userId;
if (existing) {