This commit is contained in:
Ollie Taylor
2023-07-10 21:44:55 +01:00
committed by GitHub
parent 16366ec7f3
commit 415d083f24
32 changed files with 443 additions and 297 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'svelte-podcast': minor
---
switch to jsdoc from ts
+4 -2
View File
@@ -75,12 +75,14 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Package svelte src/lib
run: yarn run package
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
id: changesets id: changesets
uses: changesets/action@v1 uses: changesets/action@v1
with: with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish publish: yarn run release
publish: yarn release
env: env:
GITHUB_TOKEN: ${{ secrets.TOKEN }} GITHUB_TOKEN: ${{ secrets.TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-1
View File
@@ -25,7 +25,6 @@
"**/*.map": true, "**/*.map": true,
"**/.turbo": true, "**/.turbo": true,
"**/.vercel": true, "**/.vercel": true,
"**/dist": true,
"**/node_modules": true, "**/node_modules": true,
"**/tsconfig.tsbuildinfo": true, "**/tsconfig.tsbuildinfo": true,
"**/.npmrc": true "**/.npmrc": true
+5 -3
View File
@@ -13,7 +13,7 @@
}, },
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && yarn package", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint", "package": "svelte-kit sync && svelte-package && publint",
"sync": "svelte-kit sync", "sync": "svelte-kit sync",
@@ -23,7 +23,7 @@
"lint:eslint": "TIMING=1 eslint . --ext .ts,.tsx,.svelte --cache", "lint:eslint": "TIMING=1 eslint . --ext .ts,.tsx,.svelte --cache",
"lint": "yarn lint:prettier && yarn lint:eslint", "lint": "yarn lint:prettier && yarn lint:eslint",
"format": "prettier --write --plugin-search-dir=. .", "format": "prettier --write --plugin-search-dir=. .",
"release": "yarn package && changeset publish", "release": "changeset publish",
"ts": "tsc --pretty --noImplicitAny --noEmit" "ts": "tsc --pretty --noImplicitAny --noEmit"
}, },
"exports": { "exports": {
@@ -36,11 +36,13 @@
"dist" "dist"
], ],
"peerDependencies": { "peerDependencies": {
"svelte": ">=3.00.0" "@sveltejs/kit": ">=1.0.0",
"svelte": ">=3.0.0"
}, },
"dependencies": { "dependencies": {
"@inqling/svelte-icons": "^3.3.2", "@inqling/svelte-icons": "^3.3.2",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"esm-env": "^1.0.0",
"just-clamp": "^4.2.0", "just-clamp": "^4.2.0",
"svelte-local-storage-store": "^0.5.0" "svelte-local-storage-store": "^0.5.0"
}, },
+2 -2
View File
@@ -1,8 +1,8 @@
<script> <script>
import { Audio } from 'svelte-podcast'; import { AudioContext } from 'svelte-podcast';
</script> </script>
<Audio /> <AudioContext />
<!-- your layout --> <!-- your layout -->
@@ -1,4 +1,4 @@
<script lang="ts"> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { user_preferences } from '../user'; import { user_preferences } from '../user';
@@ -6,11 +6,6 @@
import { audio_element } from './stores/audio-element'; import { audio_element } from './stores/audio-element';
$: $audio_element; $: $audio_element;
/* onMount(() => {
return audio_element.subscribe((el) => {
if (el) info('mounted audio element :: ', el);
});
}); */
onMount(() => { onMount(() => {
return user_preferences.subscribe((prefs) => { return user_preferences.subscribe((prefs) => {
+9
View File
@@ -0,0 +1,9 @@
/**
* Audio context module.
* @module audio
*/
export { default as AudioContext } from './context.svelte';
export { default as AudioPlayer } from './player.svelte';
export { default as AudioProgress } from './progress.svelte';
export { episode_audio, episode_details, episode_progress } from './stores';
-2
View File
@@ -1,2 +0,0 @@
export { default as Audio } from './component.svelte';
export { episode_audio, episode_details, episode_progress } from './stores';
@@ -1,13 +1,16 @@
<script lang="ts"> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { AudioProgress } from '.'; import { AudioProgress } from '.';
import { episode_audio, episode_progress } from '../audio'; import { episode_audio, episode_progress } from '../audio';
import type { EpisodeDetails } from '../audio/stores';
import { user_preferences } from '../user'; import { user_preferences } from '../user';
import { seconds_to_timestamp } from '../utility'; import { seconds_to_timestamp } from '../utility';
export let src: string | undefined; /** @type {string|undefined} */
export let metadata: EpisodeDetails = {}; export let src;
/** @type {import('../audio/stores').EpisodeDetails} */
export let metadata = {};
let mounted = false; let mounted = false;
$: is_loaded = Boolean($episode_audio?.src); $: is_loaded = Boolean($episode_audio?.src);
@@ -33,19 +36,19 @@
* Skips the audio forward by a specified number of seconds. * Skips the audio forward by a specified number of seconds.
* @param {number} seconds - The number of seconds to skip forward. * @param {number} seconds - The number of seconds to skip forward.
*/ */
const skip_forward = (seconds: number) => episode_audio.skip(seconds, 'forward'); const skip_forward = (seconds) => episode_audio.skip(seconds, 'forward');
/** /**
* Skips the audio backward by a specified number of seconds. * Skips the audio backward by a specified number of seconds.
* @param {number} seconds - The number of seconds to skip backward. * @param {number} seconds - The number of seconds to skip backward.
*/ */
const skip_back = (seconds: number) => episode_audio.skip(seconds, 'backward'); const skip_back = (seconds) => episode_audio.skip(seconds, 'backward');
/** /**
* Seeks to a specific time in the audio. * Seeks to a specific time in the audio.
* @param {number} value - The time to seek to, in seconds. * @param {number} value - The time to seek to, in seconds.
*/ */
const seek_to = (value: number) => episode_audio.seek(value); const seek_to = (value) => episode_audio.seek(value);
/** /**
* Toggles the audio playback between play and pause. * Toggles the audio playback between play and pause.
@@ -1,4 +1,4 @@
<script lang="ts"> <script>
import { episode_audio, episode_progress } from '../audio'; import { episode_audio, episode_progress } from '../audio';
import { announce } from '../utility'; import { announce } from '../utility';
@@ -6,13 +6,19 @@
let was_paused = true; let was_paused = true;
function handle_drag_start(t: string) { /**
* @param {string} t - The type of event that triggered the drag end.
*/
function handle_drag_start(t) {
announce.info('drag_start :: ', t); announce.info('drag_start :: ', t);
was_paused = $episode_audio?.is_paused || true; was_paused = $episode_audio?.is_paused || true;
episode_audio.pause(); episode_audio.pause();
} }
function handle_drag_end(t: string) { /**
* @param {string} t - The type of event that triggered the drag end.
*/
function handle_drag_end(t) {
announce.info('drag_end :: ', t); announce.info('drag_end :: ', t);
if (was_paused) return; if (was_paused) return;
else episode_audio.play(); else episode_audio.play();
+50
View File
@@ -0,0 +1,50 @@
/**
* @typedef {Object} UserPreferences
* @property {number} playback_rate - The playback rate of the audio element.
* @property {number} volume - The volume of the audio element.
*/
import { BROWSER } from 'esm-env';
import { onMount } from 'svelte';
import { get, readable } from 'svelte/store';
import { user_preferences } from '../../user';
/**
* A readable store that provides an HTMLAudioElement instance.
* @type {import('svelte/store').Readable<HTMLAudioElement | undefined>}
* @exports audio_element
*/
export const audio_element = readable(undefined, (set) => {
if (!BROWSER) return;
const ID = 'svpod--generated-audio-element';
onMount(() => {
const existing_element = document.getElementById(ID);
/** @type {HTMLAudioElement} */
const el =
existing_element instanceof HTMLAudioElement
? existing_element
: document.createElement('audio');
el.id = ID;
el.setAttribute('preload', 'metadata');
el.muted = false;
el.autoplay = false;
el.controls = false;
/** @type {UserPreferences} */
const preferences = get(user_preferences);
el.playbackRate = preferences.playback_rate;
el.volume = preferences.volume;
if (!existing_element) document.body.appendChild(el);
// @ts-expect-error - I'm not sure how to fix this using JSDOC
set(el);
});
return () => {
const element = document.getElementById(ID);
element ? element.remove() : null;
};
});
-32
View File
@@ -1,32 +0,0 @@
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { get, readable } from 'svelte/store';
import { user_preferences } from '../../user';
export const audio_element = readable<HTMLAudioElement | null>(null, (set) => {
if (!browser) return;
const ID = 'svpod--generated-audio-element';
onMount(() => {
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 = false;
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);
});
return () => {
const element = document.getElementById(ID);
element ? element.remove() : null;
};
});
@@ -1,29 +1,33 @@
import clamp from 'just-clamp'; import clamp from 'just-clamp';
import { derived, get, type Readable } from 'svelte/store'; import { derived, get } from 'svelte/store';
import { user_preferences, user_progress } from '../../user'; import { user_preferences, user_progress } from '../../user';
import { announce } from '../../utility'; import { announce } from '../../utility';
import { audio_element } from './audio-element'; import { audio_element } from './audio-element';
import { episode_details, type EpisodeDetails } from './episode-details'; import { episode_details } from './episode-details';
export type EpisodeState = { /**
will_autoplay: boolean; * Episode state object
is_paused: boolean; * @typedef {Object} EpisodeState
duration: number; * @property {boolean} will_autoplay - Whether the episode will autoplay
src: string; * @property {boolean} is_paused - Whether the episode is paused
start_at: number; * @property {number} duration - The duration of the episode
details: EpisodeDetails | null; * @property {string} src - The source of the episode
}; * @property {number} start_at - The starting point of the episode
* @property {import('./episode-details').EpisodeDetails | null} details - The details of the episode or null if there are none
*/
/** @type {Omit<EpisodeState, 'src'>} */
const default_episode_state = { const default_episode_state = {
will_autoplay: false, will_autoplay: false,
is_paused: true, is_paused: true,
duration: 0, duration: 0,
start_at: 0, start_at: 0,
details: null, details: null,
} satisfies Omit<EpisodeState, 'src'>; };
/** /**
* Episode state store * Episode state store
* @type {import('svelte/store').Readable<EpisodeState | null>}
*/ */
const episode_state = derived([audio_element, episode_details], ([$audio, $details], set) => { const episode_state = derived([audio_element, episode_details], ([$audio, $details], set) => {
if (!$audio) return set(null); if (!$audio) return set(null);
@@ -50,9 +54,11 @@ const episode_state = derived([audio_element, episode_details], ([$audio, $detai
$audio.removeEventListener('pause', set_value); $audio.removeEventListener('pause', set_value);
$audio.removeEventListener('playing', set_value); $audio.removeEventListener('playing', set_value);
}; };
}) satisfies Readable<EpisodeState | null>; });
type HANDLE_TYPE = 'toggle' | 'set'; /**
* @typedef {'toggle' | 'set'} HANDLE_TYPE
*/
/** /**
* Use audio element * Use audio element
@@ -60,7 +66,7 @@ type HANDLE_TYPE = 'toggle' | 'set';
* @returns {HTMLAudioElement} - Audio element * @returns {HTMLAudioElement} - Audio element
* @throws {string} - Error message if no audio element exists * @throws {string} - Error message if no audio element exists
*/ */
const use_element = (action: string): HTMLAudioElement => { const use_element = (action) => {
const el = get(audio_element); const el = get(audio_element);
if (!el) throw announce.warn(`could not ${action} :: no audio element exists yet`); if (!el) throw announce.warn(`could not ${action} :: no audio element exists yet`);
return el; return el;
@@ -69,9 +75,10 @@ const use_element = (action: string): HTMLAudioElement => {
/** /**
* Load audio * Load audio
* @param {string} src - Audio source * @param {string} src - Audio source
* @param {EpisodeDetails} details - Episode details * @param {import('./episode-details').EpisodeDetails } details - Episode details
* @returns {void}
*/ */
const load_audio = (src: string, details: EpisodeDetails): void => { const load_audio = (src, details) => {
user_progress.save(); user_progress.save();
const el = use_element('load'); const el = use_element('load');
el.src = src; el.src = src;
@@ -91,8 +98,9 @@ const load_audio = (src: string, details: EpisodeDetails): void => {
/** /**
* Unload audio * Unload audio
* @returns {void}
*/ */
const unload_audio = (): void => { const unload_audio = () => {
user_progress.save(); user_progress.save();
const el = use_element('unload'); const el = use_element('unload');
el.src = ''; el.src = '';
@@ -102,8 +110,9 @@ const unload_audio = (): void => {
/** /**
* Play audio * Play audio
* @param {HANDLE_TYPE} t - Handle type * @param {HANDLE_TYPE} t - Handle type
* @returns {void}
*/ */
const play_audio = (t: HANDLE_TYPE = 'set'): void => { const play_audio = (t = 'set') => {
const el = use_element('play'); const el = use_element('play');
if (t === 'toggle') { if (t === 'toggle') {
@@ -116,8 +125,9 @@ const play_audio = (t: HANDLE_TYPE = 'set'): void => {
/** /**
* Pause audio * Pause audio
* @param {HANDLE_TYPE} t - Handle type * @param {HANDLE_TYPE} t - Handle type
* @returns {void}
*/ */
const pause_audio = (t: HANDLE_TYPE = 'set'): void => { const pause_audio = (t = 'set') => {
user_progress.save(); user_progress.save();
const el = use_element('pause'); const el = use_element('pause');
@@ -131,8 +141,9 @@ const pause_audio = (t: HANDLE_TYPE = 'set'): void => {
/** /**
* Mute audio * Mute audio
* @param {HANDLE_TYPE} t - Handle type * @param {HANDLE_TYPE} t - Handle type
* @returns {void}
*/ */
const mute_audio = (t: HANDLE_TYPE = 'set'): void => { const mute_audio = (t = 'set') => {
const el = use_element('mute'); const el = use_element('mute');
if (t === 'toggle') { if (t === 'toggle') {
@@ -145,8 +156,9 @@ const mute_audio = (t: HANDLE_TYPE = 'set'): void => {
/** /**
* Unmute audio * Unmute audio
* @param {HANDLE_TYPE} t - Handle type * @param {HANDLE_TYPE} t - Handle type
* @returns {void}
*/ */
const unmute_audio = (t: HANDLE_TYPE = 'set'): void => { const unmute_audio = (t = 'set') => {
const el = use_element('unmute'); const el = use_element('unmute');
if (t === 'toggle') { if (t === 'toggle') {
@@ -159,9 +171,10 @@ const unmute_audio = (t: HANDLE_TYPE = 'set'): void => {
/** /**
* Seek to time audio * Seek to time audio
* @param {number} seconds - Seconds to seek * @param {number} seconds - Seconds to seek
* @param {'from-start' | 'from-end'} from - Seek from start or end * @param {'from-start' | 'from-end'} [from="from-start"] - Seek from start or end
* @returns {void}
*/ */
const seek_audio = (seconds: number, from: 'from-start' | 'from-end' = 'from-start'): void => { const seek_audio = (seconds, from = 'from-start') => {
const el = use_element('seek'); const el = use_element('seek');
if (from === 'from-end') { if (from === 'from-end') {
@@ -174,9 +187,10 @@ const seek_audio = (seconds: number, from: 'from-start' | 'from-end' = 'from-sta
/** /**
* Skip by about audio * Skip by about audio
* @param {number} seconds - Seconds to skip * @param {number} seconds - Seconds to skip
* @param {'forward' | 'backward'} type - Skip forward or backward * @param {'forward' | 'backward'} [type='forward'] - Skip forward or backward
* @returns {void}
*/ */
const skip_audio = (seconds: number, type: 'forward' | 'backward' = 'forward'): void => { const skip_audio = (seconds, type = 'forward') => {
const el = use_element('skip'); const el = use_element('skip');
if (type === 'backward') { if (type === 'backward') {
+12
View File
@@ -0,0 +1,12 @@
import { writable } from 'svelte/store';
/**
* @typedef {Object.<string, unknown>} EpisodeDetails
*/
/**
* Episode details store.
* @type {import('svelte/store').Writable<EpisodeDetails | null>}
* @description This store holds the metadata for a single episode.
*/
export const episode_details = writable(null);
-12
View File
@@ -1,12 +0,0 @@
import { writable } from 'svelte/store';
export interface EpisodeDetails {
[key: string]: string | number | boolean;
}
/**
* Episode details store.
*
* @returns {Writable<EpisodeDetails>} A Svelte writable store that contains metadata for the current episode.
*/
export const episode_details = writable<EpisodeDetails | null>(null);
@@ -1,23 +1,28 @@
import { derived, type Readable } from 'svelte/store'; import { seconds_to_timestamp } from 'svelte-podcast/utility';
import { seconds_to_timestamp } from '../../utility';
import { audio_element } from './audio-element'; import { audio_element } from './audio-element';
import { derived } from 'svelte/store';
export type EpisodeProgress = { /**
current_time: number; * Episode progress object.
timestamp: string; * @typedef {Object} EpisodeProgress
has_ended: boolean; * @property {number} current_time - The current time of the episode in seconds.
}; * @property {string} timestamp - The current time of the episode in the format 'mm:ss'.
* @property {boolean} has_ended - Whether the episode has ended or not.
*/
/**
* The default episode progress object.
* @type {EpisodeProgress}
*/
const default_episode_progress = { const default_episode_progress = {
current_time: 0, current_time: 0,
timestamp: '00:00', timestamp: '00:00',
has_ended: false, has_ended: false,
} satisfies EpisodeProgress; };
/** /**
* Episode progress store. * The default episode progress object.
* * @type {import('svelte/store').Readable<EpisodeProgress>}
* @returns {Readable<EpisodeProgress>} A Svelte readable store that contains the current progress of the audio episode.
*/ */
export const episode_progress = derived(audio_element, ($audio, set) => { export const episode_progress = derived(audio_element, ($audio, set) => {
if (!$audio) return set(default_episode_progress); if (!$audio) return set(default_episode_progress);
@@ -40,4 +45,4 @@ export const episode_progress = derived(audio_element, ($audio, set) => {
* Remove event listener. * Remove event listener.
*/ */
return () => $audio.removeEventListener('timeupdate', () => set_value()); return () => $audio.removeEventListener('timeupdate', () => set_value());
}) satisfies Readable<EpisodeProgress>; });
-2
View File
@@ -1,2 +0,0 @@
export { default as AudioPlayer } from './player.svelte';
export { default as AudioProgress } from './progress.svelte';
+4
View File
@@ -0,0 +1,4 @@
export * from './audio';
export { AudioPlayer as default } from './audio';
export * from './user';
export * from './utility';
-5
View File
@@ -1,5 +0,0 @@
export * from './audio';
export * from './components';
export { AudioPlayer as default } from './components';
export * from './user';
export * from './utility';
@@ -1,2 +1,7 @@
/**
* User storage module.
* @module user
*/
export * from './user-preferences'; export * from './user-preferences';
export * from './user-progress'; export * from './user-progress';
+82
View File
@@ -0,0 +1,82 @@
import clamp from 'just-clamp';
import { persisted } from 'svelte-local-storage-store';
/**
* @typedef {Object} UserPreferences
* @property {number} playback_rate - The audio player playback rate (speed)
* @property {number} volume - The audio player volume
*/
/**
* The default user preferences
* @type {UserPreferences}
*/
const DEFAULT_USER_PREFERENCES = { playback_rate: 1, volume: 1 };
/**
* The user preferences store
* @type {import('svelte/store').Writable<UserPreferences>}
*/
const USER_PREFERENCES_STORE = persisted('USER_PREFERENCE', DEFAULT_USER_PREFERENCES);
/**
* Sets the playback rate for the user
* @param {number} value - The playback rate value
* @returns {void}
*/
const set_playback_rate = (value) => {
const playback_rate = clamp(value, 0.5, 5);
return USER_PREFERENCES_STORE.update((prefs) => ({ ...prefs, playback_rate }));
};
/**
* Sets the volume for the user
* @param {number} value - The volume value
* @returns {void}
*/
const set_volume = (value) => {
const volume = clamp(value, 0, 1);
return USER_PREFERENCES_STORE.update((prefs) => ({ ...prefs, volume }));
};
/**
* Clears user preferences store
*/
const clear_store = () => USER_PREFERENCES_STORE.set(DEFAULT_USER_PREFERENCES);
/**
* @typedef {Object} UserPreferencesObject
* @property {typeof USER_PREFERENCES_STORE.subscribe} subscribe - Subscribes to the user preferences store
* @property {typeof set_playback_rate} set_playback_rate - Sets the playback rate for the user
* @property {typeof set_volume} set_volume - Sets the volume for the user
* @property {typeof clear_store} clear - Clears saved user preferences
*/
/**
* The user preferences object
* @type {UserPreferencesObject}
*/
export const user_preferences = {
/**
* Subscribes to user preferences store
*/
subscribe: USER_PREFERENCES_STORE.subscribe,
/**
* Sets the playback rate for the user
* @type {typeof set_playback_rate}
*/
set_playback_rate,
/**
* Sets the volume for the user
* @type {typeof set_volume}
*/
set_volume,
/**
* Clears user preferences store
* @type {typeof clear_store}
*/
clear: clear_store,
};
-49
View File
@@ -1,49 +0,0 @@
import clamp from 'just-clamp';
import { persisted } from 'svelte-local-storage-store';
export interface UserPreferences {
playback_rate: number;
volume: number;
}
const default_user_preferences = { playback_rate: 1, volume: 1 };
const user_preferences_store = persisted<UserPreferences>(
'USER_PREFERENCE',
default_user_preferences,
);
/**
* Sets the playback rate for the user
* @param value - The playback rate value
* @returns A Promise that resolves with the updated user preferences
*/
const set_playback_rate = (value: number) => {
const playback_rate = clamp(value, 0.5, 5);
return user_preferences_store.update((prefs) => ({ ...prefs, playback_rate }));
};
/**
* Sets the volume for the user
* @param value - The volume value
* @returns A Promise that resolves with the updated user preferences
*/
const set_volume = (value: number) => {
const volume = clamp(value, 0, 1);
return user_preferences_store.update((prefs) => ({ ...prefs, volume }));
};
/**
* Clears user preferences store
*/
const clear_store = () => user_preferences_store.set(default_user_preferences);
export const user_preferences = {
/**
* Subscribes to user preferences store
*/
subscribe: user_preferences_store.subscribe,
set_playback_rate,
set_volume,
clear: clear_store,
};
+77
View File
@@ -0,0 +1,77 @@
import { persisted } from 'svelte-local-storage-store';
import { get } from 'svelte/store';
import { episode_audio, episode_progress } from '../audio';
import { announce, use_url } from '../utility';
/**
* User progress object type.
* This object acts as a dictionary with string keys and number values.
* @typedef {Object.<string, number>} UserProgress
*/
/**
* Default user progress object
* @type {UserProgress}
*/
const DEFAULT_USER_PROGRESS = {};
import {} from 'svelte-local-storage-store';
/**
* User progress store
* @type {import('svelte/store').Writable<UserProgress>}
*/
const USER_PROGRESS_STORE = persisted('USER_PROGRESS', DEFAULT_USER_PROGRESS);
/**
* Saves user progress
* @function
* @returns {void}
*/
const save_user_progress = () => {
const audio = get(episode_audio);
if (!audio?.src) return;
announce.info('saving progress: ', audio.src);
const pathname = use_url(audio.src).pathname;
const current_time = get(episode_progress).current_time;
USER_PROGRESS_STORE.update((prev) => ({ ...prev, [pathname]: current_time }));
};
/**
* Gets user progress for a given audio source
* @function
* @param {string} src - Audio source
* @returns {number|undefined} - User progress for the given audio source, or undefined if not found
*/
const get_user_progress = (src) => {
const pathname = use_url(src).pathname;
const store = get(USER_PROGRESS_STORE);
/**@type {number|undefined} */
const value = store[pathname];
return value;
};
/**
* Clears user progress store
* @function
* @returns {void}
*/
const clear_user_progress = () => USER_PROGRESS_STORE.set(DEFAULT_USER_PROGRESS);
/**
* User progress object
* @namespace
* @property {function} subscribe - Subscribes to user progress store
* @property {function} get - Gets user progress for a given audio source
* @property {function} clear - Clears user progress store
* @property {function} save - Saves user progress
*/
export const user_progress = {
subscribe: USER_PROGRESS_STORE.subscribe,
get: get_user_progress,
clear: clear_user_progress,
save: save_user_progress,
};
-53
View File
@@ -1,53 +0,0 @@
import { persisted } from 'svelte-local-storage-store';
import { get } from 'svelte/store';
import { episode_audio, episode_progress } from '../audio';
import { announce, use_url } from '../utility';
export type UserProgress = {
[key: string]: number;
};
const default_user_progress = {};
const user_progress_store = persisted<UserProgress>('USER_PROGRESS', default_user_progress);
/**
* Saves user progress
*/
const save = () => {
const audio = get(episode_audio);
if (!audio?.src) return;
announce.info('saving progress: ', audio.src);
const pathname = use_url(audio.src).pathname;
const current_time = get(episode_progress).current_time;
user_progress_store.update((prev) => ({ ...prev, [pathname]: current_time }));
};
/**
* Gets user progress for a given audio source
* @param {string} src - Audio source
* @returns {number} - User progress for the given audio source
*/
const get_user_progress = (src: string) => {
const pathname = use_url(src).pathname;
const store = get(user_progress_store) as UserProgress;
return store[pathname];
};
/**
* Clears user progress store
*/
const clear_user_progress = () => user_progress_store.set(default_user_progress);
export const user_progress = {
/**
* Subscribes to user preferences store
*/
subscribe: user_progress_store.subscribe,
get: get_user_progress,
clear: clear_user_progress,
save,
};
+67
View File
@@ -0,0 +1,67 @@
import { DEV } from 'esm-env';
/**
* Object containing logger functions.
* @typedef {Object} Announce
* @property {function} info - Log info like events and data.
* @property {function} warn - Log warnings for developers.
* @property {function} error - Log errors for critical failures.
*/
/**
* Type of logger.
* @typedef {('info' | 'error' | 'warn')} Logger
*/
/**
* Object containing Logger functions.
*
* @type {Readonly<{[K in Logger]: Function}>}
*/
const use_logger = Object.freeze({
info: console.info,
error: console.error,
warn: console.warn,
});
/**
* Log function.
* @param {Logger} type - Type of logger.
* @param {...unknown} content - Content to log.
*/
function log(type, ...content) {
const logger = use_logger[type];
// If type is info and not in dev mode, return early
if (type === 'info' && !DEV) return;
// Log message
logger(
'%c🔊 svelte-podcast:',
'color: #FF3E00; background-color: rgba(255, 62, 0, 0.15); padding: 4px 8px; border-radius:4px;',
...content,
);
}
/**
* Object containing logger functions.
* @type {Announce}
* @exports announce
*/
export const announce = {
/**
* Log an info message.
* @param {...unknown} content - Content to log.
*/
info: (...content) => log('info', ...content),
/**
* Log a warning message.
* @param {...unknown} content - Content to log.
*/
warn: (...content) => log('warn', ...content),
/**
* Log an error message.
* @param {...unknown} content - Content to log.
*/
error: (...content) => log('error', ...content),
};
-49
View File
@@ -1,49 +0,0 @@
import { dev } from '$app/environment';
const use_logger = {
info: console.info,
error: console.error,
warn: console.warn,
};
type Logger = keyof typeof use_logger;
/**
* Log function.
* @param type - Type of logger.
* @param content - Content to log.
*/
function log(type: Logger, ...content: unknown[]) {
const logger = use_logger[type];
// If type is info and not in dev mode, return
if (type === 'info' && !dev) return;
// Log message
logger(
'%c🔊 svelte-podcast:',
'color: #FF3E00; background-color: rgba(255, 62, 0, 0.15); padding: 4px 8px; border-radius:4px;',
...content,
);
}
/**
* Object containing logger functions.
*/
export const announce = {
/**
* Log info message.
* @param content - Content to log.
*/
info: (...content: unknown[]) => log('info', ...content),
/**
* Log warning message.
* @param content - Content to log.
*/
warn: (...content: unknown[]) => log('warn', ...content),
/**
* Log error message.
* @param content - Content to log.
*/
error: (...content: unknown[]) => log('error', ...content),
};
@@ -1,3 +1,8 @@
/**
* Utility functions module.
* @module utility
*/
export * from './announce'; export * from './announce';
export * from './seconds-to-timestamp'; export * from './seconds-to-timestamp';
export * from './use-url'; export * from './use-url';
@@ -1,10 +1,10 @@
/** /**
* Returns a string timestamp in the format of "hh:mm:ss" from a given number of seconds. * Returns a string timestamp in the format of "hh:mm:ss" from a given number of seconds.
* @param seconds - The number of seconds to convert to a timestamp. * @param {number} seconds - The number of seconds to convert to a timestamp.
* @param force_hours - Whether to always include hours in the timestamp, even if it's 0. * @param {boolean} [force_hours=false] - Whether to always include hours in the timestamp, even if it's 0.
* @returns A string timestamp in the format of "hh:mm:ss". * @returns {string} A string timestamp in the format of "hh:mm:ss".
*/ */
export function seconds_to_timestamp(seconds: number, force_hours = false) { export function seconds_to_timestamp(seconds, force_hours = false) {
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const hours_remainder = seconds % 3600; const hours_remainder = seconds % 3600;
@@ -22,10 +22,10 @@ export function seconds_to_timestamp(seconds: number, force_hours = false) {
/** /**
* Returns a string representation of a number with a leading 0 if it's less than 10. * Returns a string representation of a number with a leading 0 if it's less than 10.
* @param num - The number to format. * @param {number} num - The number to format.
* @returns A string representation of the number with a leading 0 if it's less than 10. * @returns {string} A string representation of the number with a leading 0 if it's less than 10.
*/ */
function section(num: number) { function section(num) {
if (num < 10) return `0${num}`; if (num < 10) return `0${num}`;
return num.toString(); return num.toString();
} }
+31
View File
@@ -0,0 +1,31 @@
/**
* @typedef {Object} RelativeURL
* @property {string} pathname - The pathname of the URL.
* @property {string} search - The search string of the URL.
* @property {string} hash - The hash string of the URL.
* @property {URLSearchParams} searchParams - The search parameters of the URL.
*/
/**
* Converts a string to a URL object. If the string is not a valid URL, it will be appended to the Svelte website URL.
* @param {string} href - The string to convert to a URL object.
* @returns {URL} A URL object.
*/
const string_to_url = (href) => {
try {
return new URL(href);
} catch (e) {
return new URL(href, 'https://svelte.dev');
}
};
/**
* Parses a URL string and returns an object containing its pathname, search, hash, and searchParams.
* @param {string} href - The URL string to parse.
* @returns {RelativeURL} An object containing the pathname, search, hash, and searchParams of the URL.
*/
export const use_url = (href) => {
const { pathname, search, hash, searchParams } = string_to_url(href);
return { pathname, search, hash, searchParams };
};
-23
View File
@@ -1,23 +0,0 @@
/**
* Converts a string to a URL object. If the string is not a valid URL, it will be appended to the Svelte website URL.
* @param href - The string to convert to a URL object.
* @returns A URL object.
*/
const string_to_url = (href: string) => {
try {
return new URL(href);
} catch (e) {
return new URL(href, 'https://svelte.dev');
}
};
/**
* Parses a URL string and returns an object containing its pathname, search, hash, and searchParams.
* @param href - The URL string to parse.
* @returns An object containing the pathname, search, hash, and searchParams of the URL.
*/
export const use_url = (href: string) => {
const { pathname, search, hash, searchParams } = string_to_url(href);
return { pathname, search, hash, searchParams } as const;
};
+2 -2
View File
@@ -4,7 +4,7 @@
import { Github } from '@inqling/svelte-icons/simple-icons'; import { Github } from '@inqling/svelte-icons/simple-icons';
import clsx from 'clsx'; import clsx from 'clsx';
import 'highlight.js/styles/github-dark.css'; import 'highlight.js/styles/github-dark.css';
import { Audio } from 'svelte-podcast'; import { AudioContext } from 'svelte-podcast';
import '../app.postcss'; import '../app.postcss';
const page_links = [ const page_links = [
@@ -62,6 +62,6 @@
</div> </div>
</header> </header>
<Audio /> <AudioContext />
<slot /> <slot />