improves handling of user data
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { user_preferences } from '$lib/user';
|
||||
import { onMount } from 'svelte';
|
||||
import { readable } from 'svelte/store';
|
||||
import { get, readable } from 'svelte/store';
|
||||
|
||||
export const audio_element = readable<HTMLAudioElement | null>(null, (set) => {
|
||||
if (!browser) return;
|
||||
const ID = 'svelte-podcast-generated-audio-element';
|
||||
|
||||
onMount(() => {
|
||||
const el = document.createElement('audio');
|
||||
const existing_element = document.getElementById(ID) as HTMLAudioElement | null;
|
||||
const el = existing_element || document.createElement('audio');
|
||||
|
||||
el.id = ID;
|
||||
el.setAttribute('preload', 'metadata');
|
||||
el.muted = false;
|
||||
el.autoplay = true;
|
||||
el.controls = true;
|
||||
document.body.appendChild(el);
|
||||
el.controls = false;
|
||||
|
||||
const preferences = get(user_preferences);
|
||||
el.playbackRate = preferences.playback_rate;
|
||||
el.volume = preferences.volume;
|
||||
|
||||
if (!existing_element) document.body.appendChild(el);
|
||||
set(el);
|
||||
});
|
||||
|
||||
@@ -21,5 +30,3 @@ export const audio_element = readable<HTMLAudioElement | null>(null, (set) => {
|
||||
element ? element.remove() : null;
|
||||
};
|
||||
});
|
||||
|
||||
export type AudioElementStore = typeof audio_element;
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { audio_element } from '$lib/audio/audio-element';
|
||||
import { load_podcast_state } from '$lib/utility';
|
||||
import { info } from '$lib/utility/package/log';
|
||||
import { user_preferences } from '$lib/user';
|
||||
import { info, warn } from '$lib/utility/package/log';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
$: $audio_element;
|
||||
/* onMount(() => {
|
||||
return audio_element.subscribe((el) => {
|
||||
if (el) info('mounted audio element :: ', el);
|
||||
});
|
||||
}); */
|
||||
|
||||
onMount(() => {
|
||||
load_podcast_state();
|
||||
return audio_element.subscribe((el) => {
|
||||
info('loaded audio :: ', el);
|
||||
return user_preferences.subscribe((prefs) => {
|
||||
const el = get(audio_element);
|
||||
if (!el) return warn('no audio element found');
|
||||
info('setting preferences :: ', prefs);
|
||||
el.volume = prefs.volume;
|
||||
el.playbackRate = prefs.playback_rate;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { audio_element, type AudioElementStore } from '$lib/audio/audio-element';
|
||||
import { episode_details, type EpisodeDetailsStore } from '$lib/audio/episode-details';
|
||||
import { user_preferences } from '$lib/preferences';
|
||||
import { podcast_progress } from '$lib/progress';
|
||||
import { audio_element } from '$lib/audio/audio-element';
|
||||
import { episode_details } from '$lib/audio/episode-details';
|
||||
import type { EpisodeAttributes, EpisodeDetails } from '$lib/types';
|
||||
import { user_preferences, user_progress } from '$lib/user';
|
||||
import { warn } from '$lib/utility/package/log';
|
||||
import clamp from 'just-clamp';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { derived, get, type Readable } from 'svelte/store';
|
||||
|
||||
const default_episode_attributes = {
|
||||
will_autoplay: false,
|
||||
@@ -15,10 +14,9 @@ const default_episode_attributes = {
|
||||
details: null,
|
||||
} satisfies Omit<EpisodeAttributes, 'src'>;
|
||||
|
||||
const episode_attributes = derived<
|
||||
[AudioElementStore, EpisodeDetailsStore],
|
||||
EpisodeAttributes | null
|
||||
>([audio_element, episode_details], ([$audio, $details], set) => {
|
||||
type EpisodeAttributesStore = Readable<EpisodeAttributes | null>;
|
||||
|
||||
const episode_attributes = derived([audio_element, episode_details], ([$audio, $details], set) => {
|
||||
if (!$audio) return set(null);
|
||||
|
||||
function set_value() {
|
||||
@@ -30,7 +28,7 @@ const episode_attributes = derived<
|
||||
details: $details,
|
||||
will_autoplay: $audio.autoplay,
|
||||
is_paused: $audio.paused,
|
||||
start_at: podcast_progress.get_episode($audio.src)?.start_at ?? 0,
|
||||
start_at: user_progress.get($audio.src) ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ const episode_attributes = derived<
|
||||
$audio.removeEventListener('pause', set_value);
|
||||
$audio.removeEventListener('playing', set_value);
|
||||
};
|
||||
});
|
||||
}) satisfies EpisodeAttributesStore;
|
||||
|
||||
type HandleType = 'toggle' | 'set';
|
||||
|
||||
@@ -52,22 +50,25 @@ const no_element = (action: string) => warn(`could not ${action} :: no audio ele
|
||||
export const episode_audio = {
|
||||
subscribe: episode_attributes.subscribe,
|
||||
load: (src: string, details: EpisodeDetails) => {
|
||||
podcast_progress.stash_episode();
|
||||
user_progress.save();
|
||||
const el = get(audio_element);
|
||||
if (!el) return no_element('load');
|
||||
const progress = podcast_progress.get_episode(src);
|
||||
el.src = src;
|
||||
el.currentTime = progress?.start_at || 0;
|
||||
el.muted = false;
|
||||
|
||||
// using user_progress
|
||||
const start_at = user_progress.get(src) || 0;
|
||||
el.currentTime = start_at;
|
||||
|
||||
// using user_preferences
|
||||
const preferences = get(user_preferences);
|
||||
el.playbackRate = preferences.playback_rate;
|
||||
el.volume = preferences.volume;
|
||||
el.muted = false;
|
||||
|
||||
episode_details.set(details);
|
||||
},
|
||||
unload: () => {
|
||||
podcast_progress.stash_episode();
|
||||
user_progress.save();
|
||||
const el = get(audio_element);
|
||||
if (!el) return no_element('unload');
|
||||
el.src = '';
|
||||
@@ -84,7 +85,7 @@ export const episode_audio = {
|
||||
}
|
||||
},
|
||||
pause: (t?: HandleType) => {
|
||||
podcast_progress.stash_episode();
|
||||
user_progress.save();
|
||||
const el = get(audio_element);
|
||||
if (!el) return no_element('pause');
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ import type { EpisodeDetails } from '$lib/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const episode_details = writable<EpisodeDetails | null>(null);
|
||||
export type EpisodeDetailsStore = typeof episode_details;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { audio_element, type AudioElementStore } from '$lib/audio/audio-element';
|
||||
import { audio_element } from '$lib/audio/audio-element';
|
||||
import type { EpisodeProgress } from '$lib/types';
|
||||
import { secondsToTimestamp } from '$lib/utility';
|
||||
import { derived } from 'svelte/store';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
const default_episode_progress = {
|
||||
current_time: 0,
|
||||
@@ -9,21 +9,20 @@ const default_episode_progress = {
|
||||
has_ended: false,
|
||||
} satisfies EpisodeProgress;
|
||||
|
||||
export const episode_progress = derived<AudioElementStore, EpisodeProgress>(
|
||||
audio_element,
|
||||
($audio, set) => {
|
||||
if (!$audio) return set(default_episode_progress);
|
||||
type ProgressStore = Readable<EpisodeProgress>;
|
||||
|
||||
function set_value() {
|
||||
if (!$audio) return;
|
||||
set({
|
||||
current_time: $audio.currentTime,
|
||||
timestamp: secondsToTimestamp($audio.currentTime),
|
||||
has_ended: $audio.ended,
|
||||
});
|
||||
}
|
||||
export const episode_progress = derived(audio_element, ($audio, set) => {
|
||||
if (!$audio) return set(default_episode_progress);
|
||||
|
||||
$audio.addEventListener('timeupdate', () => set_value());
|
||||
return () => $audio.removeEventListener('timeupdate', () => set_value());
|
||||
},
|
||||
);
|
||||
function set_value() {
|
||||
if (!$audio) return;
|
||||
set({
|
||||
current_time: $audio.currentTime,
|
||||
timestamp: secondsToTimestamp($audio.currentTime),
|
||||
has_ended: $audio.ended,
|
||||
});
|
||||
}
|
||||
|
||||
$audio.addEventListener('timeupdate', () => set_value());
|
||||
return () => $audio.removeEventListener('timeupdate', () => set_value());
|
||||
}) satisfies ProgressStore;
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
export * from '$lib/audio';
|
||||
export * from '$lib/preferences';
|
||||
export * from '$lib/progress';
|
||||
export * from '$lib/user';
|
||||
export * from '$lib/utility';
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { user_preferences } from '$lib/preferences/db-state';
|
||||
|
||||
import type { UserPreferences } from '$lib/types';
|
||||
import { info, warn } from '$lib/utility/package/log';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const USER_PREFERENCE_KEY = 'USER_PREFERENCE' as const;
|
||||
|
||||
export function save_preferences() {
|
||||
if (!browser || !localStorage) {
|
||||
warn('localStorage not available, skipping save');
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = get(user_preferences);
|
||||
info(`Saving user preferences to localStorage`, preferences);
|
||||
localStorage.setItem(USER_PREFERENCE_KEY, JSON.stringify(preferences));
|
||||
}
|
||||
|
||||
export function load_preferences() {
|
||||
if (!browser || !localStorage) {
|
||||
warn('localStorage not available, skipping load');
|
||||
return;
|
||||
}
|
||||
const data = localStorage.getItem(USER_PREFERENCE_KEY);
|
||||
|
||||
if (!data) {
|
||||
info('No saved user preferences found');
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = JSON.parse(data) as UserPreferences;
|
||||
user_preferences.edit(preferences);
|
||||
|
||||
info(`Loaded user preferences from localStorage`, preferences);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { UserPreferences } from '$lib/types';
|
||||
import clamp from 'just-clamp';
|
||||
import { writable } from 'svelte/store';
|
||||
import { load_preferences, save_preferences } from './db-local-storage';
|
||||
|
||||
const _default_user_preferences = { playback_rate: 1, volume: 1 } satisfies UserPreferences;
|
||||
const _user_preferences = writable<UserPreferences>(_default_user_preferences);
|
||||
|
||||
const clamp_preferences = (prefs: UserPreferences) => ({
|
||||
playback_rate: clamp(prefs.playback_rate, 0.5, 5),
|
||||
volume: clamp(prefs.volume, 0, 1),
|
||||
});
|
||||
|
||||
const edit = (prefs: Partial<UserPreferences>) => {
|
||||
_user_preferences.update((prev) => clamp_preferences({ ...prev, ...prefs }));
|
||||
save_preferences();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
_user_preferences.set(_default_user_preferences);
|
||||
save_preferences();
|
||||
};
|
||||
|
||||
export const user_preferences = {
|
||||
subscribe: _user_preferences.subscribe,
|
||||
edit,
|
||||
clear,
|
||||
load: load_preferences,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './db-state';
|
||||
@@ -1 +0,0 @@
|
||||
export const _episode_progress_map = new Map<string, number>();
|
||||
@@ -1,48 +0,0 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { _episode_progress_map } from '$lib/progress/_private';
|
||||
import { error, info, warn } from '$lib/utility/package/log';
|
||||
|
||||
type SavedEpisodeProgress = { src: string; current_time: number };
|
||||
const EPISODE_PROGRESS_KEY = 'EPISODE_PROGRESS' as const;
|
||||
|
||||
export function save_podcast_progress() {
|
||||
if (!browser || !localStorage) {
|
||||
warn('localStorage not available, skipping save');
|
||||
return;
|
||||
}
|
||||
const items = [..._episode_progress_map];
|
||||
|
||||
console.log('items', items);
|
||||
|
||||
const episodes = items.reduce((prev, [src, current_time]) => {
|
||||
return [...prev, { src, current_time }];
|
||||
}, [] as SavedEpisodeProgress[]);
|
||||
|
||||
info(`Saving progress for ${episodes.length} episodes to localStorage`, episodes);
|
||||
localStorage.setItem(EPISODE_PROGRESS_KEY, JSON.stringify(episodes));
|
||||
}
|
||||
|
||||
export function load_podcast_progress() {
|
||||
if (!browser || !localStorage) {
|
||||
warn('localStorage not available, skipping load');
|
||||
return;
|
||||
}
|
||||
const data = localStorage.getItem(EPISODE_PROGRESS_KEY);
|
||||
|
||||
if (!data) {
|
||||
info('No saved episode progress found');
|
||||
return;
|
||||
}
|
||||
|
||||
const episodes = JSON.parse(data) as SavedEpisodeProgress[];
|
||||
|
||||
try {
|
||||
episodes.forEach(({ src, current_time }) => {
|
||||
_episode_progress_map.set(src, current_time);
|
||||
});
|
||||
info(`Loaded ${_episode_progress_map.size} episodes progress from localStorage`);
|
||||
} catch (e) {
|
||||
error('Error loading episode progress', e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { episode_audio, episode_progress } from '$lib/audio';
|
||||
import { _episode_progress_map } from '$lib/progress/_private';
|
||||
import type { AudioLoadOptions } from '$lib/types';
|
||||
import { info } from '$lib/utility/package/log';
|
||||
import { get } from 'svelte/store';
|
||||
import { load_podcast_progress, save_podcast_progress } from './db-local-storage';
|
||||
|
||||
const get_src_pathname = (src: string) => {
|
||||
if (src.startsWith('http')) return new URL(src).pathname;
|
||||
else return new URL(src, 'https://svelte.dev').pathname;
|
||||
};
|
||||
|
||||
const stash_episode_progress = () => {
|
||||
const audio = get(episode_audio);
|
||||
console.log('audio', audio);
|
||||
if (!audio?.src) return;
|
||||
info('saving progress: ', audio.src);
|
||||
const pathname = get_src_pathname(audio.src);
|
||||
_episode_progress_map.set(pathname, get(episode_progress).current_time);
|
||||
save_podcast_progress();
|
||||
};
|
||||
|
||||
const get_episode_progress = (src: string): Pick<AudioLoadOptions, 'start_at'> | null => {
|
||||
const pathname = get_src_pathname(src);
|
||||
const start_at = _episode_progress_map.get(pathname);
|
||||
|
||||
if (!start_at) return null;
|
||||
|
||||
info('found saved progress: ', pathname, start_at);
|
||||
|
||||
return { start_at: start_at };
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
_episode_progress_map.clear();
|
||||
save_podcast_progress();
|
||||
};
|
||||
|
||||
export const podcast_progress = {
|
||||
stash_episode: stash_episode_progress,
|
||||
get_episode: get_episode_progress,
|
||||
load: load_podcast_progress,
|
||||
clear,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './db-state';
|
||||
@@ -22,3 +22,7 @@ export interface UserPreferences {
|
||||
playback_rate: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export type UserProgress = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './preferences';
|
||||
export * from './progress';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { UserPreferences } from '$lib/types';
|
||||
import clamp from 'just-clamp';
|
||||
import { persisted } from 'svelte-local-storage-store';
|
||||
|
||||
const _default_user_preferences = { playback_rate: 1, volume: 1 } satisfies UserPreferences;
|
||||
const _user_preferences = persisted<UserPreferences>('USER_PREFERENCE', _default_user_preferences);
|
||||
|
||||
export const user_preferences = {
|
||||
subscribe: _user_preferences.subscribe,
|
||||
set: {
|
||||
playback_rate: (value: number) => {
|
||||
const playback_rate = clamp(value, 0.5, 5);
|
||||
return _user_preferences.update((prefs) => ({ ...prefs, playback_rate }));
|
||||
},
|
||||
volume: (value: number) => {
|
||||
const volume = clamp(value, 0, 1);
|
||||
return _user_preferences.update((prefs) => ({ ...prefs, volume }));
|
||||
},
|
||||
},
|
||||
clear: () => _user_preferences.set(_default_user_preferences),
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { episode_audio, episode_progress } from '$lib/audio';
|
||||
import type { UserProgress } from '$lib/types';
|
||||
import { get_pathname_from_url } from '$lib/utility/get-pathname-from-url';
|
||||
import { info } from '$lib/utility/package/log';
|
||||
import { persisted } from 'svelte-local-storage-store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const _default_user_progress = {} satisfies UserProgress;
|
||||
const _user_progress = persisted<UserProgress>('USER_PROGRESS', _default_user_progress);
|
||||
|
||||
const save = () => {
|
||||
const audio = get(episode_audio);
|
||||
if (!audio?.src) return;
|
||||
|
||||
info('saving progress: ', audio.src);
|
||||
|
||||
const pathname = get_pathname_from_url(audio.src);
|
||||
const current_time = get(episode_progress).current_time;
|
||||
_user_progress.update((prev) => ({ ...prev, [pathname]: current_time }));
|
||||
};
|
||||
|
||||
export const user_progress = {
|
||||
subscribe: _user_progress.subscribe,
|
||||
get: (src: string) => get(_user_progress)[get_pathname_from_url(src)],
|
||||
save,
|
||||
clear: () => _user_progress.set(_default_user_progress),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const get_pathname_from_url = (src: string) => {
|
||||
if (src.startsWith('http')) return new URL(src).pathname;
|
||||
else return new URL(src, 'https://svelte.dev').pathname;
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export * from './seconds-to-timestamp';
|
||||
export * from './use-state';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { user_preferences } from '$lib/preferences';
|
||||
import { podcast_progress } from '$lib/progress';
|
||||
|
||||
export function load_podcast_state() {
|
||||
podcast_progress.load();
|
||||
user_preferences.load();
|
||||
}
|
||||
Reference in New Issue
Block a user