From 7cad6ee1d627670a45846fe8d948185ffb7e4608 Mon Sep 17 00:00:00 2001 From: Ollie Taylor <13766232+OllieJT@users.noreply.github.com> Date: Sat, 25 Feb 2023 18:25:59 +0000 Subject: [PATCH] improves handling of user data --- package.json | 12 ++++-- src/lib/audio/audio-element.ts | 19 +++++++--- src/lib/audio/audio-loader.svelte | 21 ++++++++--- src/lib/audio/episode-data.ts | 35 ++++++++--------- src/lib/audio/episode-details.ts | 1 - src/lib/audio/episode-progress.ts | 35 +++++++++-------- src/lib/index.ts | 3 +- src/lib/preferences/db-local-storage.ts | 37 ------------------ src/lib/preferences/db-state.ts | 29 -------------- src/lib/preferences/index.ts | 1 - src/lib/progress/_private.ts | 1 - src/lib/progress/db-local-storage.ts | 48 ------------------------ src/lib/progress/db-state.ts | 44 ---------------------- src/lib/progress/index.ts | 1 - src/lib/types.ts | 4 ++ src/lib/user/index.ts | 2 + src/lib/user/preferences.ts | 21 +++++++++++ src/lib/user/progress.ts | 27 +++++++++++++ src/lib/utility/get-pathname-from-url.ts | 4 ++ src/lib/utility/index.ts | 1 - src/lib/utility/use-state.ts | 7 ---- src/routes/+page.svelte | 7 ++-- yarn.lock | 20 ++++++++++ 23 files changed, 154 insertions(+), 226 deletions(-) delete mode 100644 src/lib/preferences/db-local-storage.ts delete mode 100644 src/lib/preferences/db-state.ts delete mode 100644 src/lib/preferences/index.ts delete mode 100644 src/lib/progress/_private.ts delete mode 100644 src/lib/progress/db-local-storage.ts delete mode 100644 src/lib/progress/db-state.ts delete mode 100644 src/lib/progress/index.ts create mode 100644 src/lib/user/index.ts create mode 100644 src/lib/user/preferences.ts create mode 100644 src/lib/user/progress.ts create mode 100644 src/lib/utility/get-pathname-from-url.ts delete mode 100644 src/lib/utility/use-state.ts diff --git a/package.json b/package.json index 494098e..ce070d5 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,11 @@ "svelte": "^3.54.0" }, "dependencies": { - "just-clamp": "^4.2.0" + "just-clamp": "^4.2.0", + "just-filter-object": "^3.2.0", + "just-map-keys": "^2.3.0", + "just-map-values": "^3.2.0", + "svelte-local-storage-store": "^0.4.0" }, "devDependencies": { "@changesets/cli": "^2.26.0", @@ -47,14 +51,14 @@ "@sveltejs/package": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", + "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte3": "^4.0.0", - "eslint": "^8.28.0", - "prettier-plugin-svelte": "^2.8.1", "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.8.1", "publint": "^0.1.9", - "svelte-check": "^3.0.1", "svelte": "^3.54.0", + "svelte-check": "^3.0.1", "tslib": "^2.4.1", "typescript": "^4.9.3", "vite": "^4.0.0" diff --git a/src/lib/audio/audio-element.ts b/src/lib/audio/audio-element.ts index 7b3b371..5f83388 100644 --- a/src/lib/audio/audio-element.ts +++ b/src/lib/audio/audio-element.ts @@ -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(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(null, (set) => { element ? element.remove() : null; }; }); - -export type AudioElementStore = typeof audio_element; diff --git a/src/lib/audio/audio-loader.svelte b/src/lib/audio/audio-loader.svelte index 94745c3..e7e49dd 100644 --- a/src/lib/audio/audio-loader.svelte +++ b/src/lib/audio/audio-loader.svelte @@ -1,13 +1,24 @@ diff --git a/src/lib/audio/episode-data.ts b/src/lib/audio/episode-data.ts index 09ee4f2..a105bde 100644 --- a/src/lib/audio/episode-data.ts +++ b/src/lib/audio/episode-data.ts @@ -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; -const episode_attributes = derived< - [AudioElementStore, EpisodeDetailsStore], - EpisodeAttributes | null ->([audio_element, episode_details], ([$audio, $details], set) => { +type EpisodeAttributesStore = Readable; + +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'); diff --git a/src/lib/audio/episode-details.ts b/src/lib/audio/episode-details.ts index 551be6f..7942a00 100644 --- a/src/lib/audio/episode-details.ts +++ b/src/lib/audio/episode-details.ts @@ -2,4 +2,3 @@ import type { EpisodeDetails } from '$lib/types'; import { writable } from 'svelte/store'; export const episode_details = writable(null); -export type EpisodeDetailsStore = typeof episode_details; diff --git a/src/lib/audio/episode-progress.ts b/src/lib/audio/episode-progress.ts index d279206..8978f00 100644 --- a/src/lib/audio/episode-progress.ts +++ b/src/lib/audio/episode-progress.ts @@ -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( - audio_element, - ($audio, set) => { - if (!$audio) return set(default_episode_progress); +type ProgressStore = Readable; - 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; diff --git a/src/lib/index.ts b/src/lib/index.ts index 0133f35..d9ba206 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,4 +1,3 @@ export * from '$lib/audio'; -export * from '$lib/preferences'; -export * from '$lib/progress'; +export * from '$lib/user'; export * from '$lib/utility'; diff --git a/src/lib/preferences/db-local-storage.ts b/src/lib/preferences/db-local-storage.ts deleted file mode 100644 index 54e0eba..0000000 --- a/src/lib/preferences/db-local-storage.ts +++ /dev/null @@ -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); -} diff --git a/src/lib/preferences/db-state.ts b/src/lib/preferences/db-state.ts deleted file mode 100644 index e4fd3df..0000000 --- a/src/lib/preferences/db-state.ts +++ /dev/null @@ -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(_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) => { - _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, -}; diff --git a/src/lib/preferences/index.ts b/src/lib/preferences/index.ts deleted file mode 100644 index 1bed929..0000000 --- a/src/lib/preferences/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './db-state'; diff --git a/src/lib/progress/_private.ts b/src/lib/progress/_private.ts deleted file mode 100644 index 650101c..0000000 --- a/src/lib/progress/_private.ts +++ /dev/null @@ -1 +0,0 @@ -export const _episode_progress_map = new Map(); diff --git a/src/lib/progress/db-local-storage.ts b/src/lib/progress/db-local-storage.ts deleted file mode 100644 index ea5a64c..0000000 --- a/src/lib/progress/db-local-storage.ts +++ /dev/null @@ -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; - } -} diff --git a/src/lib/progress/db-state.ts b/src/lib/progress/db-state.ts deleted file mode 100644 index 117b898..0000000 --- a/src/lib/progress/db-state.ts +++ /dev/null @@ -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 | 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, -}; diff --git a/src/lib/progress/index.ts b/src/lib/progress/index.ts deleted file mode 100644 index 1bed929..0000000 --- a/src/lib/progress/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './db-state'; diff --git a/src/lib/types.ts b/src/lib/types.ts index 25c9f9c..4a96665 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -22,3 +22,7 @@ export interface UserPreferences { playback_rate: number; volume: number; } + +export type UserProgress = { + [key: string]: number; +}; diff --git a/src/lib/user/index.ts b/src/lib/user/index.ts new file mode 100644 index 0000000..ed4bb69 --- /dev/null +++ b/src/lib/user/index.ts @@ -0,0 +1,2 @@ +export * from './preferences'; +export * from './progress'; diff --git a/src/lib/user/preferences.ts b/src/lib/user/preferences.ts new file mode 100644 index 0000000..ee35947 --- /dev/null +++ b/src/lib/user/preferences.ts @@ -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('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), +}; diff --git a/src/lib/user/progress.ts b/src/lib/user/progress.ts new file mode 100644 index 0000000..1529860 --- /dev/null +++ b/src/lib/user/progress.ts @@ -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('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), +}; diff --git a/src/lib/utility/get-pathname-from-url.ts b/src/lib/utility/get-pathname-from-url.ts new file mode 100644 index 0000000..7986526 --- /dev/null +++ b/src/lib/utility/get-pathname-from-url.ts @@ -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; +}; diff --git a/src/lib/utility/index.ts b/src/lib/utility/index.ts index 6705355..9c31b07 100644 --- a/src/lib/utility/index.ts +++ b/src/lib/utility/index.ts @@ -1,2 +1 @@ export * from './seconds-to-timestamp'; -export * from './use-state'; diff --git a/src/lib/utility/use-state.ts b/src/lib/utility/use-state.ts deleted file mode 100644 index a46c1c4..0000000 --- a/src/lib/utility/use-state.ts +++ /dev/null @@ -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(); -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1cbaf01..3cc263a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,5 @@