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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user