feat(ui): persist user settings and audio position across sessions

- Replace double inline player controls with compact 'now playing' indicator when mini-bar is active
- Add user_settings PocketBase collection (auto_next, voice, speed) and audio_time field on progress
- Add getSettings/saveSettings/setAudioTime/getAudioTime server functions
- Add GET/PUT /api/settings and GET/PATCH /api/progress/audio-time API routes
- Load settings server-side in layout and apply to audioStore on mount
- Debounced 800ms effect persists settings changes to DB
- Save audio currentTime on pause/end; restore position when replaying a chapter
This commit is contained in:
Admin
2026-03-04 16:21:17 +05:00
parent b87e758303
commit 82186cfd6d
7 changed files with 363 additions and 57 deletions

View File

@@ -42,9 +42,20 @@ export interface Progress {
user_id?: string;
slug: string;
chapter: number;
audio_time?: number;
updated: string;
}
export interface UserSettings {
id?: string;
session_id: string;
user_id?: string;
auto_next: boolean;
voice: string;
speed: number;
updated?: string;
}
export interface User {
id: string;
username: string;
@@ -395,3 +406,102 @@ export async function loginUser(username: string, password: string): Promise<Use
log.info('pocketbase', 'loginUser: success', { username, role: user.role });
return user;
}
// ─── User settings ────────────────────────────────────────────────────────────
function settingsFilter(sessionId: string, userId?: string): string {
if (userId) return `user_id="${userId}"`;
return `session_id="${sessionId}"`;
}
export async function getSettings(
sessionId: string,
userId?: string
): Promise<UserSettings | null> {
return listOne<UserSettings>('user_settings', settingsFilter(sessionId, userId));
}
export async function saveSettings(
sessionId: string,
settings: { autoNext: boolean; voice: string; speed: number },
userId?: string
): Promise<void> {
const existing = await listOne<UserSettings & { id: string }>(
'user_settings',
settingsFilter(sessionId, userId)
);
const payload: Partial<UserSettings> = {
session_id: sessionId,
auto_next: settings.autoNext,
voice: settings.voice,
speed: settings.speed,
updated: new Date().toISOString()
};
if (userId) payload.user_id = userId;
if (existing) {
const res = await pbPatch(`/api/collections/user_settings/records/${existing.id}`, payload);
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'saveSettings PATCH failed', { status: res.status, body });
}
} else {
const res = await pbPost('/api/collections/user_settings/records', payload);
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'saveSettings POST failed', { status: res.status, body });
}
}
}
// ─── Audio time ───────────────────────────────────────────────────────────────
export async function setAudioTime(
sessionId: string,
slug: string,
chapter: number,
audioTime: number,
userId?: string
): Promise<void> {
const existing = await listOne<Progress & { id: string }>(
'progress',
progressFilter(sessionId, slug, userId)
);
if (!existing) {
// No progress record yet — create one with audio_time
const payload: Partial<Progress> = {
session_id: sessionId,
slug,
chapter,
audio_time: audioTime,
updated: new Date().toISOString()
};
if (userId) payload.user_id = userId;
const res = await pbPost('/api/collections/progress/records', payload);
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'setAudioTime POST failed', { slug, chapter, status: res.status, body });
}
return;
}
const res = await pbPatch(`/api/collections/progress/records/${existing.id}`, {
audio_time: audioTime,
updated: new Date().toISOString()
});
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'setAudioTime PATCH failed', { slug, chapter, status: res.status, body });
}
}
export async function getAudioTime(
sessionId: string,
slug: string,
chapter: number,
userId?: string
): Promise<number | null> {
const row = await listOne<Progress>('progress', progressFilter(sessionId, slug, userId));
if (!row || !row.audio_time) return null;
return row.audio_time;
}