diff --git a/.changeset/loud-grapes-brush.md b/.changeset/loud-grapes-brush.md new file mode 100644 index 0000000..ca115f9 --- /dev/null +++ b/.changeset/loud-grapes-brush.md @@ -0,0 +1,5 @@ +--- +'svelte-podcast': minor +--- + +refactor audio element logic diff --git a/.changeset/wise-pugs-build.md b/.changeset/wise-pugs-build.md new file mode 100644 index 0000000..1500aa7 --- /dev/null +++ b/.changeset/wise-pugs-build.md @@ -0,0 +1,5 @@ +--- +'svelte-podcast': minor +--- + +rewrite docs with code samples diff --git a/.vscode/settings.json b/.vscode/settings.json index e6dca7c..da25a27 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,11 @@ "source.organizeImports": true } }, + "[javascript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], "typescript.tsdk": "node_modules/typescript/lib", "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -33,5 +38,8 @@ "**/node_modules": true, "**/.svelte-kit": true }, - "problems.sortOrder": "severity" + "problems.sortOrder": "severity", + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative", + "todo-tree.tree.scanMode": "workspace only" } diff --git a/src/app.postcss b/src/app.postcss index ae71f23..e311cc3 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -22,7 +22,7 @@ .richtext h4 a, .richtext h5 a, .richtext h6 a { - @apply relative underline decoration-transparent decoration-solid underline-offset-2; + @apply relative break-all underline decoration-transparent decoration-solid underline-offset-2; &::before { @apply absolute -left-[1.25ch] inline-block font-light leading-normal text-mono-400; @@ -37,16 +37,19 @@ } .richtext code.hljs { - @apply p-0 text-sm; + @apply overflow-x-auto p-0 text-sm; overflow-wrap: anywhere; } .richtext .codeblock code.hljs { - @apply overflow-hidden rounded-xl border border-mono-200 p-4 font-mono text-base leading-normal shadow-2xl shadow-mono-200; + @apply overflow-auto rounded-xl border border-mono-200 p-4 font-mono text-base leading-normal shadow-2xl shadow-mono-200; } .richtext { - @apply prose prose-lg prose-slate max-w-none prose-h5:mb-1 prose-h5:mt-10 prose-h5:text-sm prose-h5:font-medium prose-h5:leading-normal prose-h5:text-primary-800 prose-code:before:content-none prose-code:after:content-none; + @apply prose prose-slate max-w-none sm:prose-lg prose-headings:px-2 prose-h5:mb-1 prose-h5:mt-10 prose-h5:text-sm prose-h5:font-medium prose-h5:leading-normal prose-h5:text-primary-800 prose-p:px-2 prose-code:before:content-none prose-code:after:content-none; + } + .richtext code:not(.hljs) { + @apply inline whitespace-pre-wrap break-all rounded-md border border-mono-200 bg-white px-1.5 py-0.5 font-mono font-normal tracking-wide text-mono-950; } .richtext td, diff --git a/src/components/example-player/index.js b/src/components/example-player/index.js index 5d2f019..ca912fc 100644 --- a/src/components/example-player/index.js +++ b/src/components/example-player/index.js @@ -1,2 +1 @@ -export { default as PlayerStack } from './player-stack.svelte'; export { default as PlayerWidget } from './player-widget.svelte'; diff --git a/src/components/example-player/player-stack.svelte b/src/components/example-player/player-stack.svelte deleted file mode 100644 index d63a491..0000000 --- a/src/components/example-player/player-stack.svelte +++ /dev/null @@ -1,444 +0,0 @@ - - - -
-
- - - - {#if episode.is_loaded} - - {:else} -
- -
- {/if} - - -
- -
- - -
- -
-
-
- -
- -
- - {#if !hide_timestamps || !hide_playback_rate} -
- {#if !hide_timestamps} -
- -
- -
- -
- -
- {/if} - - {#if !hide_playback_rate} - - {/if} -
- {/if} -
- - - diff --git a/src/components/example-player/player-widget.svelte b/src/components/example-player/player-widget.svelte index c5c78ed..b544e2b 100644 --- a/src/components/example-player/player-widget.svelte +++ b/src/components/example-player/player-widget.svelte @@ -6,7 +6,7 @@ /** @type {string | undefined} */ export let src; - /** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */ + /** @type {import('svelte-podcast/audio').AudioMetadata} */ export let metadata = {}; export let hide_duration = false; @@ -21,11 +21,105 @@ export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4]; - + + + + +
{#if !hide_skip_back} {:else} @@ -68,8 +162,8 @@ {#if !hide_current_time}
= 3600} />
{/if} @@ -80,7 +174,7 @@ {#if !hide_duration}
- +
{/if} diff --git a/src/components/example-player/utility/skip.svelte b/src/components/example-player/utility/skip.svelte index 5b503d0..b7607f4 100644 --- a/src/components/example-player/utility/skip.svelte +++ b/src/components/example-player/utility/skip.svelte @@ -153,8 +153,4 @@ transform: rotate(360deg); } } - - svg { - /* animation: Spin 1s cubic-bezier(0.5, 0.1, 0.1, 0.8) infinite; */ - } diff --git a/src/components/example-player/utility/timestamp.svelte b/src/components/example-player/utility/timestamp.svelte index f8fbcaa..afd72f9 100644 --- a/src/components/example-player/utility/timestamp.svelte +++ b/src/components/example-player/utility/timestamp.svelte @@ -1,5 +1,5 @@ -
+

{title}

- + diff --git a/src/layout/page/container.svelte b/src/layout/page/container.svelte index 4ace9a6..aeac839 100644 --- a/src/layout/page/container.svelte +++ b/src/layout/page/container.svelte @@ -6,7 +6,7 @@ diff --git a/src/layout/page/table-module.svelte b/src/layout/page/table-module.svelte deleted file mode 100644 index bb06238..0000000 --- a/src/layout/page/table-module.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -
- - - - - - - {#each rows as { method, description }} - - - - - {/each} - -
MethodDescription
- - {description}
-
diff --git a/src/layout/page/table-schema.svelte b/src/layout/page/table-schema.svelte index 66e95dc..a5315fb 100644 --- a/src/layout/page/table-schema.svelte +++ b/src/layout/page/table-schema.svelte @@ -14,9 +14,9 @@ export let rows; -
- - +
+
+ diff --git a/src/layout/sidebar.svelte b/src/layout/sidebar.svelte index ff66268..d29024b 100644 --- a/src/layout/sidebar.svelte +++ b/src/layout/sidebar.svelte @@ -36,6 +36,16 @@ /** @type {Readonly} */ export let resources; + /** + * @typedef {Object} PodcastLink + * @property {string} label - The label of the page link. + * @property {string} href - The URL of the page link. + * @property {string} src - The URL of the page link. + */ + + /** @type {Readonly} */ + export let podcasts; + $: use_is_current = (/** @type {string } */ href) => { let href_pathname = href; @@ -57,11 +67,6 @@ ) => { if (!is_current_page) return false; - if (typeof document !== 'undefined') { - const el = document.querySelector(`[id="${anchor}"]`); - if (!el) throw new Error(`No element found with id "${anchor}"`); - } - return $page.url.hash.includes(anchor); }; @@ -174,6 +179,37 @@ {/each} +
  • +
    + Svelte Podcasts +
    +
      + {#each podcasts as link} + {@const is_current = use_is_current(link.href)} +
    • + + + {link.label} + +
    • + {/each} +
    +
  • diff --git a/src/lib/audio/actions.js b/src/lib/audio/actions.js new file mode 100644 index 0000000..3f21b18 --- /dev/null +++ b/src/lib/audio/actions.js @@ -0,0 +1,165 @@ +import { BROWSER } from 'esm-env'; +import clamp from 'just-clamp'; + +import { get } from 'svelte/store'; +import { user_preferences, user_progress } from '../user'; +import { audio_attributes, use_audio_element } from './audio-element'; +import { audio_element_source } from './audio-element-source'; +import { audio_metadata } from './audio-metadata'; + +/** + * Load audio + * @param {string} src - Audio source + * @param {import('./audio-metadata').AudioMetadata } metadata - Audio metadata + * @returns {void} + */ +const load = (src, metadata = {}, autoplay = false) => { + if (!BROWSER) return; + + // Save the current progress if audio is already loaded + user_progress.save(); + + // Get user preferences + const preferences = get(user_preferences); + + // Set audio element source + audio_element_source.set({ + src, + start_at: user_progress.get(src) || 0, + playback_rate: preferences.playback_rate, + volume: preferences.volume, + autoplay: autoplay, + }); + + // Set metadata for current audio + audio_metadata.set(metadata); +}; + +/** + * Unload audio + * @returns {void} + */ +const unload = () => { + if (!BROWSER) return; + + // Save the current progress if audio is already loaded + user_progress.save(); + + // Unset audio element source + audio_element_source.set(null); + + // Unset metadata + audio_metadata.set(null); +}; + +/** + * @typedef {'toggle' | 'set'} HANDLE_TYPE + */ + +/** + * Play audio + * @param {HANDLE_TYPE} t - Handle type + * @returns {void} + */ +const play = (t = 'set') => { + const el = use_audio_element('play'); + + if (t === 'toggle') { + el.paused ? el.play() : el.pause(); + } else { + el.play(); + } +}; + +/** + * Pause audio + * @param {HANDLE_TYPE} t - Handle type + * @returns {void} + */ +const pause = (t = 'set') => { + user_progress.save(); + const el = use_audio_element('pause'); + + if (t === 'toggle') { + el.paused ? el.play() : el.pause(); + } else { + el.pause(); + } +}; + +/** + * Mute audio + * @param {HANDLE_TYPE} t - Handle type + * @returns {void} + */ +const mute = (t = 'set') => { + const el = use_audio_element('mute'); + + if (t === 'toggle') { + el.muted = !el.muted; + } else { + el.muted = true; + } +}; + +/** + * Unmute audio + * @param {HANDLE_TYPE} t - Handle type + * @returns {void} + */ +const unmute = (t = 'set') => { + const el = use_audio_element('unmute'); + + if (t === 'toggle') { + el.muted = !el.muted; + } else { + el.muted = false; + } +}; + +/** + * Seek to time audio + * @param {number} seconds - Seconds to seek + * @param {'from-start' | 'from-end'} [from="from-start"] - Seek from start or end + * @returns {void} + */ +const seek = (seconds, from = 'from-start') => { + const el = use_audio_element('seek'); + + if (from === 'from-end') { + el.currentTime = clamp(0, el.duration, el.duration - seconds); + } else { + el.currentTime = clamp(0, el.duration, seconds); + } +}; + +/** + * Skip by about audio + * @param {number} seconds - Seconds to skip + * @param {'forward' | 'backward'} [type='forward'] - Skip forward or backward + * @returns {void} + */ +const skip = (seconds, type = 'forward') => { + const el = use_audio_element('skip'); + + if (type === 'backward') { + el.currentTime = clamp(0, el.duration, el.currentTime - seconds); + } else { + el.currentTime = clamp(0, el.duration, el.currentTime + seconds); + } +}; + +/** + * Audio controls + */ +export const audio = { + subscribe: audio_attributes.subscribe, + load, + unload, + play, + pause, + mute, + unmute, + seek, + skip, +}; diff --git a/src/lib/audio/audio-element-source.js b/src/lib/audio/audio-element-source.js new file mode 100644 index 0000000..704eb71 --- /dev/null +++ b/src/lib/audio/audio-element-source.js @@ -0,0 +1,15 @@ +import { writable } from 'svelte/store'; + +// TODO: implement autoplay + +/** + * @typedef {Object} AudioSource + * @property {string} src - A path or URL to the audio file + * @property {number} start_at - The starting point of the audio + * @property {number} [playback_rate=1] - The playback speed of the audio + * @property {number} [volume=1] - The volume of the audio + * @property {boolean} [autoplay=false] - Whether the audio should play as soon as it's loaded + */ + +/** @type {import('svelte/store').Writable} */ +export const audio_element_source = writable(null); diff --git a/src/lib/audio/audio-element.js b/src/lib/audio/audio-element.js new file mode 100644 index 0000000..660a666 --- /dev/null +++ b/src/lib/audio/audio-element.js @@ -0,0 +1,132 @@ +import { BROWSER } from 'esm-env'; +import { derived, get } from 'svelte/store'; +import { announce } from '../internal'; +import { seconds_to_timestamp } from '../utility'; +import { audio_element_source } from './audio-element-source'; + +/** + * @typedef {import('svelte/store').Readable} ReadableAudioElement + */ + +/** + * @typedef {import('../user').UserPreferences} Preferences + */ + +/** + * @typedef {(value: HTMLAudioElement | undefined) => void} SetAudioElement + */ + +const ELEMENT_ID = 'svpod--generated-audio-element'; + +/** @type {ReadableAudioElement} */ +export const audio_element = derived( + [audio_element_source], + + ([$audio_element_src], /** @type {SetAudioElement} */ set) => { + // If not in a browser environment, return early. + if (BROWSER) { + // Find an existing HTMLAudioElement or create a new one. + const found = document.getElementById(ELEMENT_ID); + const el = + found instanceof HTMLAudioElement + ? found + : document.createElement('audio'); + + // Set the HTMLAudioElement's attributes. + el.id = ELEMENT_ID; + el.setAttribute('preload', 'metadata'); + el.muted = false; + el.autoplay = false; + el.controls = false; + + if ($audio_element_src) { + el.src = $audio_element_src.src; + el.currentTime = $audio_element_src.start_at; + el.playbackRate = $audio_element_src.playback_rate || 1; + el.volume = $audio_element_src.volume || 1; + el.autoplay = $audio_element_src.autoplay || false; + } + + // If a new HTMLAudioElement was created, append it to the body. + if (!found) document.body.appendChild(el); + + // Define a function to set the HTMLAudioElement and call it. + const handle_update = () => { + set(el); + $audio_element_src?.autoplay && el.play(); + announce.info('Updating audio element'); + }; + + handle_update(); + + // Add event listeners to the HTMLAudioElement to update the store when it changes. + el.addEventListener('loadeddata', handle_update); + el.addEventListener('pause', handle_update); + el.addEventListener('playing', handle_update); + el.addEventListener('timeupdate', handle_update); + + // Return a function to clean up the HTMLAudioElement when the store unsubscribes. + return () => { + // Remove the event listeners from the HTMLAudioElement. + el.removeEventListener('loadeddata', handle_update); + el.removeEventListener('pause', handle_update); + el.removeEventListener('playing', handle_update); + el.removeEventListener('timeupdate', handle_update); + + // Remove the HTMLAudioElement from the DOM. + el ? el.remove() : null; + }; + } else { + set(undefined); + return () => null; + } + }, + undefined, +); + +/** + * Use audio element + * @param {string} action - Action being performed + * @returns {HTMLAudioElement} - Audio element + * @throws {string} - Error message if no audio element exists + */ +export const use_audio_element = (action) => { + const el = get(audio_element); + if (!el) { + throw announce.error(`could not ${action} :: no audio element found`); + } + return el; +}; + +/** + * @typedef {Object} AudioAttributes + * @property {boolean} is_loaded - Whether the audio element is loaded or not + * @property {boolean} is_paused - Whether the audio element is paused or not + * @property {number} current_time - The current time of the audio element in seconds + * @property {number} duration - The duration of the audio element in seconds + * @property {string} timestamp_current - The current time of the audio element in timestamp format (hh:mm:ss) + * @property {string} timestamp_end - The end time of the audio element in timestamp format (hh:mm:ss) + */ + +/** @type {import('svelte/store').Readable} */ +export const audio_attributes = derived(audio_element, ($el, set) => { + if (!$el) { + return set({ + is_loaded: false, + is_paused: true, + current_time: 0, + duration: 0, + timestamp_current: seconds_to_timestamp(0), + timestamp_end: seconds_to_timestamp(0), + }); + } + + set({ + is_loaded: Boolean($el.src), + is_paused: $el.paused, + current_time: $el.currentTime, + duration: $el.duration, + timestamp_current: seconds_to_timestamp($el.currentTime), + timestamp_end: seconds_to_timestamp($el.duration), + }); +}); diff --git a/src/lib/audio/audio-metadata.js b/src/lib/audio/audio-metadata.js new file mode 100644 index 0000000..3cf3d13 --- /dev/null +++ b/src/lib/audio/audio-metadata.js @@ -0,0 +1,12 @@ +import { writable } from 'svelte/store'; + +/** + * @typedef {Object.} AudioMetadata + */ + +/** + * Audio metadata store. + * @type {import('svelte/store').Writable} + * @description This store holds the metadata for a single audio source. + */ +export const audio_metadata = writable(null); diff --git a/src/lib/audio/context.svelte b/src/lib/audio/context.svelte deleted file mode 100644 index d0f902d..0000000 --- a/src/lib/audio/context.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - diff --git a/src/lib/audio/index.js b/src/lib/audio/index.js index d6e4354..ec1bb42 100644 --- a/src/lib/audio/index.js +++ b/src/lib/audio/index.js @@ -3,7 +3,9 @@ * @module audio */ -export { default as AudioContext } from './context.svelte'; +export * from './actions'; +export * from './audio-element'; +export * from './audio-element-source'; +export * from './audio-metadata'; export { default as AudioPlayer } from './player.svelte'; export { default as AudioProgress } from './progress.svelte'; -export { episode_audio, episode_details, episode_progress } from './stores'; diff --git a/src/lib/audio/player.svelte b/src/lib/audio/player.svelte index 881928c..755b4db 100644 --- a/src/lib/audio/player.svelte +++ b/src/lib/audio/player.svelte @@ -1,71 +1,48 @@ diff --git a/src/lib/audio/progress.svelte b/src/lib/audio/progress.svelte index 38eaa73..eccec57 100644 --- a/src/lib/audio/progress.svelte +++ b/src/lib/audio/progress.svelte @@ -1,6 +1,7 @@ -{#if $episode_audio && $episode_progress} +{#if $audio_attributes.is_loaded} episode_audio.seek(e.currentTarget.valueAsNumber)} + max={$audio_attributes.duration} + value={$audio_attributes.current_time} + on:change={(e) => handle_seek_to(e.currentTarget.valueAsNumber)} on:touchstart={() => handle_drag_start('touchstart')} on:mousedown={() => handle_drag_start('mousedown')} on:touchend={() => handle_drag_end('touchend')} diff --git a/src/lib/audio/stores/audio-element.js b/src/lib/audio/stores/audio-element.js deleted file mode 100644 index 64d8d22..0000000 --- a/src/lib/audio/stores/audio-element.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @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} - * @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); - - /* - BUG: This is a hack to get around poor types. - The input and output types are correct, so this is safe for now. - */ - // @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; - }; -}); diff --git a/src/lib/audio/stores/episode-data.js b/src/lib/audio/stores/episode-data.js deleted file mode 100644 index 42036a1..0000000 --- a/src/lib/audio/stores/episode-data.js +++ /dev/null @@ -1,225 +0,0 @@ -import clamp from 'just-clamp'; -import { derived, get } from 'svelte/store'; -import { user_preferences, user_progress } from '../../user'; -import { announce } from '../../internal'; -import { audio_element } from './audio-element'; -import { episode_details } from './episode-details'; - -/** - * Episode state object - * @typedef {Object} EpisodeState - * @property {boolean} will_autoplay - Whether the episode will autoplay - * @property {boolean} is_paused - Whether the episode is paused - * @property {number} duration - The duration of the episode - * @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} */ -const default_episode_state = { - will_autoplay: false, - is_paused: true, - duration: 0, - start_at: 0, - details: null, -}; - -/** - * Episode state store - * @type {import('svelte/store').Readable} - */ -const episode_state = derived( - [audio_element, episode_details], - ([$audio, $details], set) => { - if (!$audio) return set(null); - - function set_value() { - if (!$audio?.src) return null; - set({ - ...default_episode_state, - src: $audio.src, - duration: $audio.duration, - details: $details, - will_autoplay: $audio.autoplay, - is_paused: $audio.paused, - start_at: user_progress.get($audio.src) ?? 0, - }); - } - - $audio.addEventListener('loadeddata', set_value); - $audio.addEventListener('pause', set_value); - $audio.addEventListener('playing', set_value); - - return () => { - $audio.removeEventListener('loadeddata', set_value); - $audio.removeEventListener('pause', set_value); - $audio.removeEventListener('playing', set_value); - }; - }, -); - -/** - * @typedef {'toggle' | 'set'} HANDLE_TYPE - */ - -/** - * Use audio element - * @param {string} action - Action to perform - * @returns {HTMLAudioElement} - Audio element - * @throws {string} - Error message if no audio element exists - */ -const use_element = (action) => { - const el = get(audio_element); - if (!el) - throw announce.warn(`could not ${action} :: no audio element exists yet`); - return el; -}; - -/** - * Load audio - * @param {string} src - Audio source - * @param {import('./episode-details').EpisodeDetails } details - Episode details - * @returns {void} - */ -const load_audio = (src, details) => { - user_progress.save(); - const el = use_element('load'); - el.src = src; - 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; - - episode_details.set(details); -}; - -/** - * Unload audio - * @returns {void} - */ -const unload_audio = () => { - user_progress.save(); - const el = use_element('unload'); - el.src = ''; - episode_details.set(null); -}; - -/** - * Play audio - * @param {HANDLE_TYPE} t - Handle type - * @returns {void} - */ -const play_audio = (t = 'set') => { - const el = use_element('play'); - - if (t === 'toggle') { - el.paused ? el.play() : el.pause(); - } else { - el.play(); - } -}; - -/** - * Pause audio - * @param {HANDLE_TYPE} t - Handle type - * @returns {void} - */ -const pause_audio = (t = 'set') => { - user_progress.save(); - const el = use_element('pause'); - - if (t === 'toggle') { - el.paused ? el.play() : el.pause(); - } else { - el.pause(); - } -}; - -/** - * Mute audio - * @param {HANDLE_TYPE} t - Handle type - * @returns {void} - */ -const mute_audio = (t = 'set') => { - const el = use_element('mute'); - - if (t === 'toggle') { - el.muted = !el.muted; - } else { - el.muted = true; - } -}; - -/** - * Unmute audio - * @param {HANDLE_TYPE} t - Handle type - * @returns {void} - */ -const unmute_audio = (t = 'set') => { - const el = use_element('unmute'); - - if (t === 'toggle') { - el.muted = !el.muted; - } else { - el.muted = false; - } -}; - -/** - * Seek to time audio - * @param {number} seconds - Seconds to seek - * @param {'from-start' | 'from-end'} [from="from-start"] - Seek from start or end - * @returns {void} - */ -const seek_audio = (seconds, from = 'from-start') => { - const el = use_element('seek'); - - if (from === 'from-end') { - el.currentTime = clamp(0, el.duration, el.duration - seconds); - } else { - el.currentTime = clamp(0, el.duration, seconds); - } -}; - -/** - * Skip by about audio - * @param {number} seconds - Seconds to skip - * @param {'forward' | 'backward'} [type='forward'] - Skip forward or backward - * @returns {void} - */ -const skip_audio = (seconds, type = 'forward') => { - const el = use_element('skip'); - - if (type === 'backward') { - el.currentTime = clamp(0, el.duration, el.currentTime - seconds); - } else { - el.currentTime = clamp(0, el.duration, el.currentTime + seconds); - } -}; - -/** - * Episode audio object - */ -export const episode_audio = { - /** - * Subscribes to episode audio store - */ - subscribe: episode_state.subscribe, - - load: load_audio, - unload: unload_audio, - - play: play_audio, - pause: pause_audio, - mute: mute_audio, - unmute: unmute_audio, - seek: seek_audio, - skip: skip_audio, -}; diff --git a/src/lib/audio/stores/episode-details.js b/src/lib/audio/stores/episode-details.js deleted file mode 100644 index 677bfe5..0000000 --- a/src/lib/audio/stores/episode-details.js +++ /dev/null @@ -1,12 +0,0 @@ -import { writable } from 'svelte/store'; - -/** - * @typedef {Object.} EpisodeDetails - */ - -/** - * Episode details store. - * @type {import('svelte/store').Writable} - * @description This store holds the metadata for a single episode. - */ -export const episode_details = writable(null); diff --git a/src/lib/audio/stores/episode-progress.js b/src/lib/audio/stores/episode-progress.js deleted file mode 100644 index 61a1ed7..0000000 --- a/src/lib/audio/stores/episode-progress.js +++ /dev/null @@ -1,48 +0,0 @@ -import { seconds_to_timestamp } from 'svelte-podcast/utility'; -import { audio_element } from './audio-element'; -import { derived } from 'svelte/store'; - -/** - * Episode progress object. - * @typedef {Object} EpisodeProgress - * @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 = { - current_time: 0, - timestamp: '00:00', - has_ended: false, -}; - -/** - * The default episode progress object. - * @type {import('svelte/store').Readable} - */ -export const episode_progress = derived(audio_element, ($audio, set) => { - if (!$audio) return set(default_episode_progress); - - /** - * Set episode progress value. - */ - function set_value() { - if (!$audio) return; - set({ - current_time: $audio.currentTime, - timestamp: seconds_to_timestamp($audio.currentTime), - has_ended: $audio.ended, - }); - } - - $audio.addEventListener('timeupdate', () => set_value()); - - /** - * Remove event listener. - */ - return () => $audio.removeEventListener('timeupdate', () => set_value()); -}); diff --git a/src/lib/audio/stores/index.js b/src/lib/audio/stores/index.js deleted file mode 100644 index cb0148e..0000000 --- a/src/lib/audio/stores/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './audio-element'; -export * from './episode-data'; -export * from './episode-details'; -export * from './episode-progress'; diff --git a/src/lib/index.js b/src/lib/index.js index 05b5d68..13dd3f3 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1,4 +1,7 @@ -export * from './audio'; -export { AudioPlayer as default } from './audio'; +import { default as AudioPlayer } from './audio/player.svelte'; + +export * from './audio/actions'; export * from './user'; -export * from './utility'; + +export { AudioPlayer }; +export default AudioPlayer; diff --git a/src/lib/user/user-progress.js b/src/lib/user/user-progress.js index 13ce01b..8aefee8 100644 --- a/src/lib/user/user-progress.js +++ b/src/lib/user/user-progress.js @@ -1,7 +1,7 @@ import { persisted } from 'svelte-local-storage-store'; import { get } from 'svelte/store'; -import { episode_audio, episode_progress } from '../audio'; -import { use_url, announce } from '../internal'; +import { audio_element } from '../audio'; +import { announce, use_url } from '../internal'; /** * User progress object type. @@ -14,7 +14,7 @@ import { use_url, announce } from '../internal'; * @type {UserProgress} */ const DEFAULT_USER_PROGRESS = {}; -import {} from 'svelte-local-storage-store'; + /** * User progress store * @type {import('svelte/store').Writable} @@ -27,13 +27,13 @@ const USER_PROGRESS_STORE = persisted('USER_PROGRESS', DEFAULT_USER_PROGRESS); * @returns {void} */ const save_user_progress = () => { - const audio = get(episode_audio); + const audio = get(audio_element); 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; + const current_time = audio.currentTime; USER_PROGRESS_STORE.update((prev) => ({ ...prev, [pathname]: current_time, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ae6838f..6a4deb9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,7 +3,6 @@ import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte'; import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte'; import github from 'svelte-highlight/styles/github'; - import { AudioContext } from 'svelte-podcast'; import { circIn, circOut } from 'svelte/easing'; import { fade } from 'svelte/transition'; import '../app.postcss'; @@ -18,21 +17,54 @@ label: 'Setup', href: '/setup', sections: [ - { label: 'installation', anchor: 'installation' }, - { label: 'Loader', anchor: 'add_loader' }, + { label: 'Installation', anchor: 'installation' }, + { label: 'Audio Sources', anchor: 'audio_sources' }, + { label: 'Type Safety', anchor: 'type_safety' }, ], }, { label: 'API', href: '/api', sections: [ - { label: 'Episode Audio', anchor: 'episode_audio' }, - { label: 'Load Audio Source', anchor: 'load_audio_source' }, - { label: 'Episode Details', anchor: 'episode_details' }, - { label: 'Episode Progress', anchor: 'episode_progress' }, - { label: 'Seconds To Timestamp', anchor: 'seconds_to_timestamp' }, - { label: 'User Preferences', anchor: 'user_preferences' }, - { label: 'User Progress', anchor: 'user_progress' }, + { label: 'audio', anchor: 'audio' }, + { label: 'audio_data', anchor: 'audio_data' }, + { label: 'audio.subscribe', anchor: 'audio.subscribe' }, + { label: 'audio.load', anchor: 'audio.load' }, + { label: 'audio.unload', anchor: 'audio.unload' }, + { label: 'audio.play', anchor: 'audio.play' }, + { label: 'audio.pause', anchor: 'audio.pause' }, + { label: 'audio.mute', anchor: 'audio.mute' }, + { label: 'audio.unmute', anchor: 'audio.unmute' }, + { label: 'audio.seek', anchor: 'audio.seek' }, + { label: 'audio.skip', anchor: 'audio.skip' }, + { label: 'user_preferences', anchor: 'user_preferences' }, + { label: 'user_preferences_data', anchor: 'user_preferences_data' }, + { + label: 'user_preferences.subscribe', + anchor: 'user_preferences.subscribe', + }, + { + label: 'user_preferences.set_playback_rate', + anchor: 'user_preferences.set_playback_rate', + }, + { + label: 'user_preferences.set_volume', + anchor: 'user_preferences.set_volume', + }, + { + label: 'user_preferences.clear', + anchor: 'user_preferences.clear', + }, + { label: 'user_progress', anchor: 'user_progress' }, + { label: 'user_progress_data', anchor: 'user_progress_data' }, + { + label: 'user_progress.subscribe', + anchor: 'user_progress.subscribe', + }, + { label: 'user_progress.get', anchor: 'user_progress.get' }, + { label: 'user_progress.clear', anchor: 'user_progress.clear' }, + { label: 'user_progress.save', anchor: 'user_progress.save' }, + { label: 'seconds_to_timestamp', anchor: 'seconds_to_timestamp' }, ], }, { @@ -55,6 +87,19 @@ }, ]); + const podcast_links = Object.freeze([ + { + label: 'Svelte Radio', + href: 'https://www.svelteradio.com/', + src: '/svelte-radio.png', + }, + { + label: 'Syntax', + href: 'https://syntax.fm/', + src: '/syntax.png', + }, + ]); + let is_menu_open = false; const open_menu = () => (is_menu_open = true); @@ -65,8 +110,6 @@ {@html github} - -
    {/if} @@ -134,6 +178,7 @@ class="border-r border-mono-200" pages={page_links} resources={resource_links} + podcasts={podcast_links} /> @@ -179,7 +224,7 @@
    -
    +
    diff --git a/src/routes/api/+page.svelte b/src/routes/api/+page.svelte index 040a497..4891973 100644 --- a/src/routes/api/+page.svelte +++ b/src/routes/api/+page.svelte @@ -1,242 +1,289 @@ - -
    - -

    Methods

    + +
    + + + - - - -

    EpisodeState

    - - - - + +

    - All you need to load an episode is a URL to an audio file. - svelte-podcast uses a html audio element under the hood, so any - audio file compatible with the autio element is also compatible with - this package. + Subscribe to specific changes in the audio attributes. This will + return a readable store that you can use to access the attributes.

    - -

    Using a remote file (URL)

    - -

    - An audio url could be a URL to an MP3 file from an RSS - feed, like this: - https://media.transistor.fm/27a058c9/27b595e2.mp3. It - could also be a path to a static file on your server. -

    - -
    - +
    +
    + -

    Using a local file (relative path)

    - +

    - If you're using SvelteKit, you can store static files in - the /static directory. When your site is built, everything in the - static directory will be at the root of your site. If you have a - file in /static/episides/episode-01.mp3 you could load - it as /episides/episode-01.mp3 + Load a new audio source. This will stop the current audio source and + replace it with the new one.

    +
    + +
    +
    -
    - + +

    + Unload the current audio source. This will stop the current audio + source and remove it from the audio state. +

    +
    + +
    +
    + + +

    Set or toggle the play state.

    +
    + +
    +
    + + +

    Set or toggle the pause state.

    +
    + +
    +
    + + +

    Set or toggle the mute state.

    +
    + +
    +
    + + +

    Set or toggle the unmute state.

    +
    + +
    +
    + + +

    + Seek to a specific time in the audio. The `from` parameter + determines whether the time is relative to the start or end of the + audio. +

    +
    + +
    +
    + + +

    + Skip forward or backward in the audio by a specific number of + seconds. +

    +
    +
    -
    -

    Methods

    +
    + + + - EpisodeDetails\n)', - description: - 'You can update the audio state. This is useful if you want to update the state based on the previous state.', - }, - ]} - /> + + +

    + Subscribe to changes in the user preferences. This will return a + readable store that you can use to access the user preferences. +

    +
    + +
    +
    - -

    EpisodeState

    -
    - `} language={lang_ts} /> -
    -

    - Episode state holds the metadata for the current episode. You derermine - the shape of the this data. -

    + +

    + Set the playback rate. This will update the user preferences and + apply the new playback rate to the audio. +

    +
    + +
    +
    -

    - For complete type safety, we recommend defining this type yourself in a - .d.ts file. In SvelteKit, you should do this in - /src/app.d.ts - . -

    + +

    + Set the volume. This will update the user preferences and apply the + new volume to the audio. +

    +
    + +
    +
    -

    Override EpisodeState

    -
    - -
    + +

    + Clear the user preferences. This will reset the user preferences to + the default values. +

    +
    + +
    +
    - -
    - -

    Methods

    - EpisodeProgress\n)', - description: - 'You can update the audio state. This is useful if you want to update the state based on the previous state.', - }, - ]} - /> + +
    + +

    + User progress is a record where the keys are audio sources, and the + values are timestamps in seconds. Progress is retrieved by matching + the source, and returning the timestamp. +

    - -

    EpisodeProgress

    - +
    + `} language={lang_ts} /> +
    +
    + + + +

    + Subscribe to changes in the user progress. This will return a + readable store that you can use to access the user progress. +

    +
    + +
    +
    + + +

    + Get the user progress for a given audio source. This will return the + timestamp in seconds. +

    +
    + +
    +
    + + +

    + Clear the user progress. This will reset the user progress to the + default values. +

    +
    + +
    +
    + + +

    + Save the user progress for a given audio source. This will update + the user progress with the new timestamp. +

    +
    + +
    +
    @@ -261,89 +308,4 @@
    - - -
    - -

    Methods

    - - - -

    UserPreferences

    - -
    - - -
    - -

    Methods

    - - - -

    UserProgress

    -

    - User progress is a record where the keys are audio sources, and the - values are timestamps in seconds. Progress is retrieved by matching the - source, and returning the timestamp. -

    - -
    - `} language={lang_ts} /> -
    -
    diff --git a/src/routes/api/code/audio-load.js b/src/routes/api/code/audio-load.js new file mode 100644 index 0000000..d48cd37 --- /dev/null +++ b/src/routes/api/code/audio-load.js @@ -0,0 +1,17 @@ +import { audio } from 'svelte-podcast'; + +// load a new audio source +audio.load( + // path to audio file + '/episode-377.mp3', + + // custom metadata + { + title: 'Supper Club × Rich Harris, Author of Svelte', + guests: ['Rich Harris'], + hosts: ['Wes Bos', 'Scott Tolinski'], + }, + + // autoplay + true, +); diff --git a/src/routes/api/code/audio-mute.js b/src/routes/api/code/audio-mute.js new file mode 100644 index 0000000..789fae9 --- /dev/null +++ b/src/routes/api/code/audio-mute.js @@ -0,0 +1,9 @@ +import { audio } from 'svelte-podcast'; + +// mute the current audio source +// do nothing if it's already muted +audio.mute('set'); + +// mute the current audio if it's unmuted +// unmute the current audio if it's muted +audio.mute('toggle'); diff --git a/src/routes/api/code/audio-pause.js b/src/routes/api/code/audio-pause.js new file mode 100644 index 0000000..bfa3dd8 --- /dev/null +++ b/src/routes/api/code/audio-pause.js @@ -0,0 +1,9 @@ +import { audio } from 'svelte-podcast'; + +// pause the current audio source +// do nothing if it's already paused +audio.pause('set'); + +// pause the current audio if it's playing +// play the current audio if it's paused +audio.pause('toggle'); diff --git a/src/routes/api/code/audio-play.js b/src/routes/api/code/audio-play.js new file mode 100644 index 0000000..0fd33c2 --- /dev/null +++ b/src/routes/api/code/audio-play.js @@ -0,0 +1,9 @@ +import { audio } from 'svelte-podcast'; + +// play the current audio source +// do nothing if it's already playing +audio.play('set'); + +// play the current audio if it's paused +// pause the current audio if it's playing +audio.play('toggle'); diff --git a/src/routes/api/code/audio-seek.js b/src/routes/api/code/audio-seek.js new file mode 100644 index 0000000..17a2e2a --- /dev/null +++ b/src/routes/api/code/audio-seek.js @@ -0,0 +1,7 @@ +import { audio } from 'svelte-podcast'; + +// go to 200 seconds from the start +audio.seek(200); + +// go to 200 seconds before the end +audio.seek(200, 'from-end'); diff --git a/src/routes/api/code/audio-skip.js b/src/routes/api/code/audio-skip.js new file mode 100644 index 0000000..0b89e7e --- /dev/null +++ b/src/routes/api/code/audio-skip.js @@ -0,0 +1,7 @@ +import { audio } from 'svelte-podcast'; + +// skip forward 30 seconds from the current position +audio.skip(30); + +// skip backward 30 seconds from the current position +audio.skip(30, 'backward'); diff --git a/src/routes/api/code/audio-subscribe.svelte b/src/routes/api/code/audio-subscribe.svelte new file mode 100644 index 0000000..36a9d2f --- /dev/null +++ b/src/routes/api/code/audio-subscribe.svelte @@ -0,0 +1,16 @@ + + +

    Current timestamp {attributes.timestamp_current}.

    + +

    {attributes.is_paused ? 'Paused...' : 'Playing!'}.

    diff --git a/src/routes/api/code/audio-unload.js b/src/routes/api/code/audio-unload.js new file mode 100644 index 0000000..af801f9 --- /dev/null +++ b/src/routes/api/code/audio-unload.js @@ -0,0 +1,4 @@ +import { audio } from 'svelte-podcast'; + +// unload the current audio source +audio.unload(); diff --git a/src/routes/api/code/audio-unmute.js b/src/routes/api/code/audio-unmute.js new file mode 100644 index 0000000..51e1789 --- /dev/null +++ b/src/routes/api/code/audio-unmute.js @@ -0,0 +1,9 @@ +import { audio } from 'svelte-podcast'; + +// unmute the current audio source +// do nothing if it's already unmuted +audio.unmute('set'); + +// unmute the current audio if it's muted +// mute the current audio if it's unmuted +audio.unmute('toggle'); diff --git a/src/routes/api/code/index.js b/src/routes/api/code/index.js index 009ae3b..716e32a 100644 --- a/src/routes/api/code/index.js +++ b/src/routes/api/code/index.js @@ -1,15 +1,48 @@ +import { format_code } from '../../../layout/helper'; +import { default as audio_load_src } from './audio-load.js?raw'; +import { default as audio_mute_src } from './audio-mute.js?raw'; +import { default as audio_pause_src } from './audio-pause.js?raw'; +import { default as audio_play_src } from './audio-play.js?raw'; +import { default as audio_seek_src } from './audio-seek.js?raw'; +import { default as audio_skip_src } from './audio-skip.js?raw'; +import { default as audio_subscribe_src } from './audio-subscribe.svelte?raw'; +import { default as audio_unload_src } from './audio-unload.js?raw'; +import { default as audio_unmute_src } from './audio-unmute.js?raw'; import { default as load_audio_after_click_src } from './load-audio-after-click.svelte?raw'; import { default as load_audio_after_mount_src } from './load-audio-after-mount.svelte?raw'; -import { default as load_audio_local_src } from './load-audio-local?raw'; -import { default as load_audio_remote_src } from './load-audio-remote?raw'; -import { default as override_episode_state_src } from './override-episode-state?raw'; import { default as seconds_to_timestamp_src } from './seconds-to-timestamp?raw'; - -const format_code = (/** @type {string} */ code) => code.replaceAll('\t', ' '); +import { default as user_preferences_clear_src } from './user_preferences-clear?raw'; +import { default as user_preferences_set_playback_rate_src } from './user_preferences-set_playback_rate?raw'; +import { default as user_preferences_set_volume_src } from './user_preferences-set_volume?raw'; +import { default as user_preferences_subscribe_src } from './user_preferences-subscribe.svelte?raw'; +import { default as user_progress_clear_src } from './user_progress-clear?raw'; +import { default as user_progress_get_src } from './user_progress-get?raw'; +import { default as user_progress_save_src } from './user_progress-save?raw'; +import { default as user_progress_subscribe_src } from './user_progress-subscribe.svelte?raw'; export const load_audio_after_click = format_code(load_audio_after_click_src); export const load_audio_after_mount = format_code(load_audio_after_mount_src); -export const load_audio_local = format_code(load_audio_local_src); -export const load_audio_remote = format_code(load_audio_remote_src); -export const override_episode_state = format_code(override_episode_state_src); export const seconds_to_timestamp = format_code(seconds_to_timestamp_src); +export const audio_load = format_code(audio_load_src); +export const audio_play = format_code(audio_play_src); +export const audio_unload = format_code(audio_unload_src); +export const audio_pause = format_code(audio_pause_src); +export const audio_skip = format_code(audio_skip_src); +export const audio_seek = format_code(audio_seek_src); +export const audio_mute = format_code(audio_mute_src); +export const audio_unmute = format_code(audio_unmute_src); +export const user_preferences_clear = format_code(user_preferences_clear_src); +export const user_preferences_set_playback_rate = format_code( + user_preferences_set_playback_rate_src, +); +export const user_preferences_set_volume = format_code( + user_preferences_set_volume_src, +); +export const user_preferences_subscribe = format_code( + user_preferences_subscribe_src, +); +export const audio_subscribe = format_code(audio_subscribe_src); +export const user_progress_clear = format_code(user_progress_clear_src); +export const user_progress_get = format_code(user_progress_get_src); +export const user_progress_save = format_code(user_progress_save_src); +export const user_progress_subscribe = format_code(user_progress_subscribe_src); diff --git a/src/routes/api/code/load-audio-after-click.svelte b/src/routes/api/code/load-audio-after-click.svelte index f20855e..1fa0fde 100644 --- a/src/routes/api/code/load-audio-after-click.svelte +++ b/src/routes/api/code/load-audio-after-click.svelte @@ -1,11 +1,11 @@ - + diff --git a/src/routes/api/code/load-audio-after-mount.svelte b/src/routes/api/code/load-audio-after-mount.svelte index 63cfadb..28230f3 100644 --- a/src/routes/api/code/load-audio-after-mount.svelte +++ b/src/routes/api/code/load-audio-after-mount.svelte @@ -1,10 +1,10 @@ + +

    Listening at {preferences.playback_rate * 100}% speed.

    + +

    Volume set to {preferences.volume * 100}%.

    diff --git a/src/routes/api/code/user_progress-clear.js b/src/routes/api/code/user_progress-clear.js new file mode 100644 index 0000000..b4180d8 --- /dev/null +++ b/src/routes/api/code/user_progress-clear.js @@ -0,0 +1,4 @@ +import { user_progress } from 'svelte-podcast'; + +// Clear the user's progress for ALL episodes. +user_progress.clear(); diff --git a/src/routes/api/code/user_progress-get.js b/src/routes/api/code/user_progress-get.js new file mode 100644 index 0000000..93990fa --- /dev/null +++ b/src/routes/api/code/user_progress-get.js @@ -0,0 +1,4 @@ +import { user_progress } from 'svelte-podcast'; + +// Get the user's progress for a specific episode. +user_progress.get('https://example.com/episodes/1'); diff --git a/src/routes/api/code/user_progress-save.js b/src/routes/api/code/user_progress-save.js new file mode 100644 index 0000000..bf34982 --- /dev/null +++ b/src/routes/api/code/user_progress-save.js @@ -0,0 +1,4 @@ +import { user_progress } from 'svelte-podcast'; + +// Save the user's progress for the current episode. +user_progress.save(); diff --git a/src/routes/api/code/user_progress-subscribe.svelte b/src/routes/api/code/user_progress-subscribe.svelte new file mode 100644 index 0000000..f5ce5fa --- /dev/null +++ b/src/routes/api/code/user_progress-subscribe.svelte @@ -0,0 +1,16 @@ + + +

    + Listened to {progress['https://example.com/episodes/1']} seconds of episode 1. +

    diff --git a/src/routes/examples/+page.svelte b/src/routes/examples/+page.svelte index 4412096..714dc05 100644 --- a/src/routes/examples/+page.svelte +++ b/src/routes/examples/+page.svelte @@ -1,12 +1,8 @@ - + +

    Work in progress!

    @@ -82,45 +66,4 @@
    - -
    - - -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte index d7b4245..af58fe4 100644 --- a/src/routes/setup/+page.svelte +++ b/src/routes/setup/+page.svelte @@ -1,9 +1,15 @@ @@ -17,37 +23,82 @@
    -
    -

    - Important: The AudioLoader component does - not - render any UI. However it is required to use svelte-podcast. + +

    +

    + To get started, you'll need an audio file. velte-podcast uses a html {' element under the hood, so any file compatible with the audio element + will work.

    - This component is responsible for loading the audio sources and - persisting the audio state between page loads. This means that your - audio will continue playing even if the user refreshes the page, or you - use different UI for the media player. + You can provide your audio files via absolute URL's - hosted online, or + relative paths - hosted within your project.

    +

    Using a remote file (URL)

    +

    - Add the AudioLoader component to your app. It should be as close to the - root of your app as possible to ensure the audio state behaves as - expected in nested routes. + An audio url could be a URL to an MP3 file from an RSS feed, + like this: + https://media.transistor.fm/27a058c9/27b595e2.mp3. It + could also be a path to a static file on your server.

    -
      -
    • You must have one of these for svelte-podcast to work.
    • -
    • You should also only load one instance of this at a time
    • -
    • - We recommend you loading it at the base of your app in your - layout.svelte file. -
    • -
    -
    - + +
    + +

    Using a local file (relative path)

    + +

    + If you're using SvelteKit, you can store static files in the + /static directory. When your site is built, everything in the static + directory will be at the root of your site. If you have a file in + /static/episides/episode-01.mp3 + you could load it as /episides/episode-01.mp3 +

    + +
    + +
    + +

    Additional options

    + +

    + Along side your audio source, you can optionally define arbitrary + metadata for you to use in your player, and determine if the episode + should autoplay once loaded. Learn more in the api docs +

    + +
    +
    + +
    +

    + Svelte podast makes it easy for you to associate arbitrary metadata + with each audio source. We recommend defining the shape of your + metadata in a .d.ts file. +

    + +

    + In this example, we'll assume that your metadata consists of an episode + title, episode artwork, and an optional episode guest name. +

    + +
    + +
    + +

    + If you're using SvelteKit, we recommend keeping this in src/app.d.ts. +

    +
    diff --git a/src/routes/setup/code/add-audio-loader.svelte b/src/routes/setup/code/add-audio-loader.svelte index cdba189..9cc1fe5 100644 --- a/src/routes/setup/code/add-audio-loader.svelte +++ b/src/routes/setup/code/add-audio-loader.svelte @@ -1,7 +1,4 @@ - - diff --git a/src/routes/setup/code/index.js b/src/routes/setup/code/index.js index bcdc2a2..571484e 100644 --- a/src/routes/setup/code/index.js +++ b/src/routes/setup/code/index.js @@ -1,6 +1,14 @@ import { format_code } from '$src/layout/helper'; import { default as add_audio_loader_src } from './add-audio-loader.svelte?raw'; import { default as install_src } from './install.sh?raw'; +import { default as load_audio_advanced_src } from './load-audio-advanced?raw'; +import { default as load_audio_local_src } from './load-audio-local?raw'; +import { default as load_audio_remote_src } from './load-audio-remote?raw'; +import { default as override_episode_state_src } from './override-episode-state?raw'; export const add_audio_loader = format_code(add_audio_loader_src); export const install = format_code(install_src); +export const override_episode_state = format_code(override_episode_state_src); +export const load_audio_local = format_code(load_audio_local_src); +export const load_audio_remote = format_code(load_audio_remote_src); +export const load_audio_advanced = format_code(load_audio_advanced_src); diff --git a/src/routes/setup/code/load-audio-advanced.js b/src/routes/setup/code/load-audio-advanced.js new file mode 100644 index 0000000..f3ffd95 --- /dev/null +++ b/src/routes/setup/code/load-audio-advanced.js @@ -0,0 +1,14 @@ +import { audio } from 'svelte-podcast'; + +audio.load('/episode-audio.mp3'); + +audio.load( + 'https://media.transistor.fm/27a058c9/27b595e2.mp3', + { + title: 'SvelteKit-superforms with Andreas Söderlund', + artwork: + 'https://images.transistor.fm/file/transistor/images/show/12899/medium_1597678946-artwork.jpg', + guest_name: 'Andreas Söderlund', + }, + false, +); diff --git a/src/routes/setup/code/load-audio-local.js b/src/routes/setup/code/load-audio-local.js new file mode 100644 index 0000000..3a15bae --- /dev/null +++ b/src/routes/setup/code/load-audio-local.js @@ -0,0 +1,3 @@ +import { audio } from 'svelte-podcast'; + +audio.load('/episode-audio.mp3'); diff --git a/src/routes/setup/code/load-audio-remote.js b/src/routes/setup/code/load-audio-remote.js new file mode 100644 index 0000000..1de7c79 --- /dev/null +++ b/src/routes/setup/code/load-audio-remote.js @@ -0,0 +1,3 @@ +import { audio } from 'svelte-podcast'; + +audio.load('https://media.transistor.fm/27a058c9/27b595e2.mp3'); diff --git a/src/routes/api/code/override-episode-state.ts b/src/routes/setup/code/override-episode-state.ts similarity index 71% rename from src/routes/api/code/override-episode-state.ts rename to src/routes/setup/code/override-episode-state.ts index e61e7cf..0da531e 100644 --- a/src/routes/api/code/override-episode-state.ts +++ b/src/routes/setup/code/override-episode-state.ts @@ -2,9 +2,9 @@ declare module 'svelte-podcast' { interface EpisodeDetails { - // define your own properties here title: string; - artwork?: string; + artwork: string; + guest_name: string; } } diff --git a/static/svelte-radio.png b/static/svelte-radio.png new file mode 100644 index 0000000..7bbd4dc Binary files /dev/null and b/static/svelte-radio.png differ diff --git a/static/syntax.png b/static/syntax.png new file mode 100644 index 0000000..367f05a Binary files /dev/null and b/static/syntax.png differ diff --git a/tsconfig.json b/tsconfig.json index 1dc0a8c..bf98482 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,16 +3,48 @@ "compilerOptions": { "allowJs": true, "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, "sourceMap": true, - "strict": true, - // Additional Rules + // Enable top-level await, and other modern ESM features. + "target": "ESNext", + "module": "ESNext", + // Enable node-style module resolution, for things like npm package imports. + "moduleResolution": "node", + // Enable JSON imports. + "resolveJsonModule": true, + // Enable stricter transpilation for better output. + "isolatedModules": true, + // Astro directly run TypeScript code, no transpilation needed. + "noEmit": true, + // Report an error when importing a file using a casing different from the casing on disk. + "forceConsistentCasingInFileNames": true, + // Properly support importing CJS modules in ESM + "esModuleInterop": true, + // Skip typechecking libraries and .d.ts files + "skipLibCheck": true, + + "strict": true, + // Error when a value import is only used as a type. + "importsNotUsedAsValues": "error", + + // Report errors for fallthrough cases in switch statements + "noFallthroughCasesInSwitch": true, + // Force functions designed to override their parent class to be specified as `override`. + "noImplicitOverride": true, + // Force functions to specify that they can return `undefined` if a possible code path does not return a value. + "noImplicitReturns": true, + // Report an error when a variable is declared but never used. + "noUnusedLocals": true, + // Report an error when a parameter is declared but never used. + "noUnusedParameters": true, + // Force the usage of the indexed syntax to access fields declared using an index signature. "noUncheckedIndexedAccess": true, - "noEmit": true + // Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value. + "exactOptionalPropertyTypes": false, + // Report an error for unreachable code instead of just a warning. + "allowUnreachableCode": false, + // Report an error for unused labels instead of just a warning. + "allowUnusedLabels": false } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias //
    Property Type Description