Cleanup lib exports (#50)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte-podcast': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Restructures package exports
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'svelte-podcast': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
- improved export structure
|
||||||
|
- simplify audio methods
|
||||||
|
- improve typescript types
|
||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<meta name="theme-color" content="#f97316" />
|
<meta name="theme-color" content="#f97316" />
|
||||||
<meta name="color-scheme" content="light" />
|
<meta name="color-scheme" content="light" />
|
||||||
|
|
||||||
<link rel="bookmark" title="svelte-podcast Docs" />
|
<link rel="bookmark" title="Svelte Podcast Docs" />
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
Pause,
|
Pause,
|
||||||
@@ -8,14 +8,8 @@
|
|||||||
} from '$src/components/icon';
|
} from '$src/components/icon';
|
||||||
import { AudioPlayer, user_preferences } from 'svelte-podcast';
|
import { AudioPlayer, user_preferences } from 'svelte-podcast';
|
||||||
|
|
||||||
/** @type {string | undefined} */
|
|
||||||
export let src;
|
|
||||||
export let skip_back = 30;
|
export let skip_back = 30;
|
||||||
export let skip_forward = 10;
|
export let skip_forward = 10;
|
||||||
|
|
||||||
/** @type {import('svelte-podcast/audio').AudioMetadata} */
|
|
||||||
export let metadata = {};
|
|
||||||
|
|
||||||
export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
|
export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,14 +31,20 @@
|
|||||||
</style>
|
</style>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<AudioPlayer {src} {metadata} let:Player let:action let:attributes>
|
<AudioPlayer
|
||||||
|
let:PlayerProgress
|
||||||
|
let:skip_by
|
||||||
|
let:play
|
||||||
|
let:preference
|
||||||
|
let:attributes
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="svpod-container {$$restProps.class}"
|
class="svpod-container {$$restProps.class}"
|
||||||
data-loaded={attributes.is_loaded ? 'true' : 'false'}
|
data-loaded={attributes.is_loaded ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
<!-- Player Action: Skip back -->
|
<!-- Player Action: Skip back -->
|
||||||
<button
|
<button
|
||||||
on:click={() => action.skip_back(skip_back)}
|
on:click={() => skip_by(skip_back, 'backward')}
|
||||||
class="svpod-skip"
|
class="svpod-skip"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Skip back {skip_back} seconds"
|
aria-label="Skip back {skip_back} seconds"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<!-- Player Action: Toggle play / pause -->
|
<!-- Player Action: Toggle play / pause -->
|
||||||
{#if attributes.is_loaded}
|
{#if attributes.is_loaded}
|
||||||
<button
|
<button
|
||||||
on:click={() => action.toggle()}
|
on:click={() => play('toggle')}
|
||||||
class="svpod-toggle"
|
class="svpod-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={attributes.is_paused ? 'Play' : 'Pause'}
|
aria-label={attributes.is_paused ? 'Play' : 'Pause'}
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<svelte:component this={attributes.is_paused ? Play : Pause} />
|
<svelte:component this={attributes.is_paused ? Play : Pause} />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="svpod-toggle">
|
<div class="svpod-toggle text-white">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
<span class="svpod--a11y">Waiting for audio...</span>
|
<span class="svpod--a11y">Waiting for audio...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
<!-- Player Action: Skip forward -->
|
<!-- Player Action: Skip forward -->
|
||||||
<button
|
<button
|
||||||
on:click={() => action.skip_forward(skip_forward)}
|
on:click={() => skip_by(skip_forward, 'forward')}
|
||||||
class="svpod-skip"
|
class="svpod-skip"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Skip forward {skip_forward} seconds"
|
aria-label="Skip forward {skip_forward} seconds"
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<!-- Player Component: Timeline -->
|
<!-- Player Component: Timeline -->
|
||||||
<div class="svpod--timeline">
|
<div class="svpod--timeline">
|
||||||
<Player.Progress />
|
<PlayerProgress />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Data: Total duration timestamp -->
|
<!-- Player Data: Total duration timestamp -->
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
value={$user_preferences.playback_rate}
|
value={$user_preferences.playback_rate}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
const value = parseFloat(e.currentTarget.value);
|
const value = parseFloat(e.currentTarget.value);
|
||||||
action.set_playback_rate(value);
|
preference.set_playback_rate(value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each playback_rate_values as value}
|
{#each playback_rate_values as value}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { AudioProgress } from '.';
|
||||||
|
import { use_audio_element } from './_internal_/audio-element';
|
||||||
|
import { audio } from './actions';
|
||||||
|
import { audio_attributes } from './attributes';
|
||||||
|
import { user_preferences } from './user-preferences';
|
||||||
|
import { user_progress } from './user-progress';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
use_audio_element('initialise');
|
||||||
|
dispatch('ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
$: $audio;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot
|
||||||
|
PlayerProgress={AudioProgress}
|
||||||
|
attributes={$audio_attributes}
|
||||||
|
preference={{
|
||||||
|
set_playback_rate: user_preferences.set_playback_rate,
|
||||||
|
set_volume: user_preferences.set_volume,
|
||||||
|
save_progress: user_progress.save,
|
||||||
|
}}
|
||||||
|
play={audio.play}
|
||||||
|
mute={audio.mute}
|
||||||
|
seek_to={audio.seek_to}
|
||||||
|
skip_by={audio.skip_by}
|
||||||
|
/>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import clamp from 'just-clamp';
|
import clamp from 'just-clamp';
|
||||||
import { announce } from '../internal';
|
import { announce } from './_internal_/announce';
|
||||||
import { audio_attributes, audio_element } from './audio-element';
|
import { audio_element } from './_internal_/audio-element';
|
||||||
|
import { audio_attributes } from './attributes';
|
||||||
export let step = 10;
|
export let step = 10;
|
||||||
|
|
||||||
let was_paused = true;
|
let was_paused = true;
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
/**
|
/**
|
||||||
* @param {string} t - The type of event that triggered the drag end.
|
* @param {string} t - The type of event that triggered the drag end.
|
||||||
*/
|
*/
|
||||||
function handle_drag_start(t) {
|
function handle_drag_start(t: string) {
|
||||||
announce.info('drag_start :: ', t);
|
announce.info('drag_start :: ', t);
|
||||||
was_paused = $audio_attributes.is_paused;
|
was_paused = $audio_attributes.is_paused;
|
||||||
$audio_element?.pause();
|
$audio_element?.pause();
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
/**
|
/**
|
||||||
* @param {string} t - The type of event that triggered the drag end.
|
* @param {string} t - The type of event that triggered the drag end.
|
||||||
*/
|
*/
|
||||||
function handle_drag_end(t) {
|
function handle_drag_end(t: string) {
|
||||||
announce.info('drag_end :: ', t);
|
announce.info('drag_end :: ', t);
|
||||||
if (was_paused) return;
|
if (was_paused) return;
|
||||||
else $audio_element?.play();
|
else $audio_element?.play();
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
* @param {number} seconds - Seconds to seek
|
* @param {number} seconds - Seconds to seek
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const handle_seek_to = (seconds) => {
|
const handle_seek_to = (seconds: number) => {
|
||||||
const el = $audio_element;
|
const el = $audio_element;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
|
||||||
|
const use_logger = Object.freeze({
|
||||||
|
info: console.info,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
});
|
||||||
|
|
||||||
|
const base_styles = `padding: 4px 8px; border-radius:4px;`;
|
||||||
|
const logger_styles = {
|
||||||
|
error: `color: #FFFFFF; background-color: #FF0055; ${base_styles}`,
|
||||||
|
warn: `color: #662200; background-color: #FFBB33; ${base_styles}`,
|
||||||
|
info: `color: #80EAFF; background-color: #132686; ${base_styles}`,
|
||||||
|
} satisfies { [K in keyof typeof use_logger]: string };
|
||||||
|
|
||||||
|
function log(type: keyof typeof use_logger, ...content: unknown[]) {
|
||||||
|
// If type is info and not in dev mode, return early
|
||||||
|
if (type === 'info' && !DEV) return;
|
||||||
|
|
||||||
|
const logger = use_logger[type];
|
||||||
|
const styles = logger_styles[type];
|
||||||
|
|
||||||
|
// Log message
|
||||||
|
logger('%c🔊 svelte-podcast:', styles, ...content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const announce = {
|
||||||
|
/**
|
||||||
|
* Log an info message.
|
||||||
|
* @param {...unknown} content - Content to log.
|
||||||
|
*/
|
||||||
|
info: (...content: unknown[]) => log('info', ...content),
|
||||||
|
/**
|
||||||
|
* Log a warning message.
|
||||||
|
* @param {...unknown} content - Content to log.
|
||||||
|
*/
|
||||||
|
warn: (...content: unknown[]) => log('warn', ...content),
|
||||||
|
/**
|
||||||
|
* Log an error message.
|
||||||
|
* @param {...unknown} content - Content to log.
|
||||||
|
*/
|
||||||
|
error: (...content: unknown[]) => log('error', ...content),
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { BROWSER } from 'esm-env';
|
||||||
|
import { derived, get, type Readable } from 'svelte/store';
|
||||||
|
import { announce } from './announce';
|
||||||
|
import { audio_state } from './audio-state';
|
||||||
|
|
||||||
|
const ELEMENT_ID = 'svpod--generated-audio-element';
|
||||||
|
|
||||||
|
/** A derived Svelte store containing an HTMLAudioElement or undefined. */
|
||||||
|
export const audio_element: Readable<HTMLAudioElement | null> = derived(
|
||||||
|
[audio_state],
|
||||||
|
|
||||||
|
([$audio_state], set) => {
|
||||||
|
// If not in a browser environment, return early.
|
||||||
|
if (!BROWSER) {
|
||||||
|
set(null);
|
||||||
|
return () => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_state) {
|
||||||
|
el.src = $audio_state.src;
|
||||||
|
el.currentTime = $audio_state.start_at;
|
||||||
|
el.playbackRate = $audio_state.playback_rate || 1;
|
||||||
|
el.volume = $audio_state.volume || 1;
|
||||||
|
el.autoplay = $audio_state.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_state?.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;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
null as HTMLAudioElement | null,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use audio element
|
||||||
|
* @param action - Action being performed
|
||||||
|
*/
|
||||||
|
export const use_audio_element = (action: string): HTMLAudioElement => {
|
||||||
|
const el = get(audio_element);
|
||||||
|
if (!el) {
|
||||||
|
throw announce.error(`could not ${action} :: no audio element found`);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
import type { AudioMetadata } from '../metadata';
|
||||||
|
|
||||||
|
/** A writable Svelte store containing the audio metadata. */
|
||||||
|
export const audio_metadata: Writable<AudioMetadata | null> = writable(null);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
// TODO: implement autoplay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representing the state of an audio player.
|
||||||
|
*/
|
||||||
|
interface AudioState {
|
||||||
|
src: string;
|
||||||
|
start_at: number;
|
||||||
|
playback_rate?: number;
|
||||||
|
volume?: number;
|
||||||
|
autoplay?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A writable Svelte store containing the audio state. */
|
||||||
|
export const audio_state: Writable<AudioState | null> = writable(null);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions module.
|
||||||
|
* @module Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './audio-element';
|
||||||
|
export * from './audio-metadata';
|
||||||
|
export * from './audio-state';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* An object representing a relative URL.
|
||||||
|
*/
|
||||||
|
interface RelativeURL {
|
||||||
|
pathname: string;
|
||||||
|
search: string;
|
||||||
|
hash: string;
|
||||||
|
searchParams: URLSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function string_to_url(href: string): URL {
|
||||||
|
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 function use_url(href: string): RelativeURL {
|
||||||
|
const { pathname, search, hash, searchParams } = string_to_url(href);
|
||||||
|
|
||||||
|
return { pathname, search, hash, searchParams };
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { BROWSER } from 'esm-env';
|
||||||
|
import clamp from 'just-clamp';
|
||||||
|
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { use_audio_element } from './_internal_/audio-element';
|
||||||
|
import { audio_metadata } from './_internal_/audio-metadata';
|
||||||
|
import { audio_state } from './_internal_/audio-state';
|
||||||
|
import { audio_attributes } from './attributes';
|
||||||
|
import type { AudioMetadata } from './metadata';
|
||||||
|
import { user_preferences } from './user-preferences';
|
||||||
|
import { user_progress } from './user-progress';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load audio
|
||||||
|
* @param src - Audio source
|
||||||
|
* @param metadata - Audio metadata
|
||||||
|
*/
|
||||||
|
const load_src = (src: string, metadata: AudioMetadata, 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_state.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
|
||||||
|
*/
|
||||||
|
const unload_src = () => {
|
||||||
|
if (!BROWSER) return;
|
||||||
|
|
||||||
|
// Save the current progress if audio is already loaded
|
||||||
|
user_progress.save();
|
||||||
|
|
||||||
|
// Unset audio element source
|
||||||
|
audio_state.set(null);
|
||||||
|
|
||||||
|
// Unset metadata
|
||||||
|
audio_metadata.set(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play / Pause audio
|
||||||
|
* @param playing - Set or toggle the play state
|
||||||
|
*/
|
||||||
|
const play = (playing: boolean | 'toggle' = 'toggle') => {
|
||||||
|
const el = use_audio_element('play');
|
||||||
|
|
||||||
|
if (typeof playing === 'boolean') {
|
||||||
|
playing ? el.play() : el.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playing === 'toggle') {
|
||||||
|
el.paused ? el.play() : el.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute / Unmute audio
|
||||||
|
* @param muted - Set or toggle the mute state
|
||||||
|
*/
|
||||||
|
const mute = (muted: boolean | 'toggle' = 'toggle') => {
|
||||||
|
const el = use_audio_element('mute');
|
||||||
|
|
||||||
|
if (typeof muted === 'boolean') {
|
||||||
|
el.muted = muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muted === 'toggle') {
|
||||||
|
el.muted = !el.muted;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeks to a specific time in the audio.
|
||||||
|
* @param seconds Timestamp in seconds
|
||||||
|
* @param from Seek from-start or from-end
|
||||||
|
*/
|
||||||
|
const seek_to = (
|
||||||
|
seconds: number,
|
||||||
|
from: 'from-start' | 'from-end' = '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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the audio forward or backward by a specified number of seconds.
|
||||||
|
* @param seconds - Time to skip in seconds
|
||||||
|
* @param type [type='forward'] - Skip forward or backward
|
||||||
|
*/
|
||||||
|
const skip_by = (seconds: number, type: 'forward' | 'backward' = '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,
|
||||||
|
src: {
|
||||||
|
load: load_src,
|
||||||
|
unload: unload_src,
|
||||||
|
},
|
||||||
|
play,
|
||||||
|
mute,
|
||||||
|
seek_to,
|
||||||
|
skip_by,
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { derived, type Readable } from 'svelte/store';
|
||||||
|
import { audio_element } from './_internal_/audio-element';
|
||||||
|
import { audio_metadata } from './_internal_/audio-metadata';
|
||||||
|
import type { AudioMetadata } from './metadata';
|
||||||
|
import { format_seconds } from './utility/format-seconds';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representing the attributes of an audio element.
|
||||||
|
*/
|
||||||
|
export interface AudioAttributes {
|
||||||
|
is_loaded: boolean;
|
||||||
|
is_paused: boolean;
|
||||||
|
current_time: number;
|
||||||
|
duration: number;
|
||||||
|
timestamp_current: string;
|
||||||
|
timestamp_end: string;
|
||||||
|
metadata: AudioMetadata | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A readable Svelte store containing the audio attributes. */
|
||||||
|
export const audio_attributes: Readable<AudioAttributes> = derived(
|
||||||
|
[audio_element, audio_metadata],
|
||||||
|
([$el, $meta], set) => {
|
||||||
|
if (!$el) {
|
||||||
|
return set({
|
||||||
|
is_loaded: false,
|
||||||
|
is_paused: true,
|
||||||
|
current_time: 0,
|
||||||
|
duration: 0,
|
||||||
|
timestamp_current: format_seconds.to_timestamp(0),
|
||||||
|
timestamp_end: format_seconds.to_timestamp(0),
|
||||||
|
metadata: null,
|
||||||
|
} satisfies AudioAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
is_loaded: Boolean($el.src),
|
||||||
|
is_paused: $el.paused,
|
||||||
|
current_time: $el.currentTime,
|
||||||
|
duration: $el.duration,
|
||||||
|
timestamp_current: format_seconds.to_timestamp($el.currentTime),
|
||||||
|
timestamp_end: format_seconds.to_timestamp($el.duration),
|
||||||
|
metadata: $meta,
|
||||||
|
} satisfies AudioAttributes);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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<AudioSource | null>} */
|
|
||||||
export const audio_element_source = writable(null);
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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<HTMLAudioElement | undefined>} 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<AudioAttributes>} */
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object.<string, unknown>} AudioMetadata
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Audio metadata store.
|
|
||||||
* @type {import('svelte/store').Writable<AudioMetadata | null>}
|
|
||||||
* @description This store holds the metadata for a single audio source.
|
|
||||||
*/
|
|
||||||
export const audio_metadata = writable(null);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Audio context module.
|
|
||||||
* @module audio
|
|
||||||
*/
|
|
||||||
|
|
||||||
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';
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { AudioProgress, audio_attributes } from '.';
|
|
||||||
import { audio } from '../audio';
|
|
||||||
import { user_preferences } from '../user';
|
|
||||||
|
|
||||||
/** @type {string|undefined} */
|
|
||||||
export let src;
|
|
||||||
|
|
||||||
/** @type {import('./audio-metadata').AudioMetadata} */
|
|
||||||
export let metadata = {};
|
|
||||||
|
|
||||||
$: src && audio.load(src, metadata || {});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips the audio forward by a specified number of seconds.
|
|
||||||
* @param {number} seconds - The number of seconds to skip forward.
|
|
||||||
*/
|
|
||||||
const skip_forward = (seconds) => audio.skip(seconds, 'forward');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips the audio backward by a specified number of seconds.
|
|
||||||
* @param {number} seconds - The number of seconds to skip backward.
|
|
||||||
*/
|
|
||||||
const skip_back = (seconds) => audio.skip(seconds, 'backward');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seeks to a specific time in the audio.
|
|
||||||
* @param {number} value - The time to seek to, in seconds.
|
|
||||||
*/
|
|
||||||
const seek_to = (value) => audio.seek(value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the audio playback between play and pause.
|
|
||||||
*/
|
|
||||||
const toggle = () => audio.play('toggle');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playing the audio.
|
|
||||||
*/
|
|
||||||
const play = () => audio.play('set');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses the audio.
|
|
||||||
*/
|
|
||||||
const pause = () => audio.pause('set');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot
|
|
||||||
Player={{ Progress: AudioProgress }}
|
|
||||||
action={{
|
|
||||||
set_playback_rate: user_preferences.set_playback_rate,
|
|
||||||
set_volume: user_preferences.set_volume,
|
|
||||||
skip_forward,
|
|
||||||
skip_back,
|
|
||||||
seek_to,
|
|
||||||
toggle,
|
|
||||||
play,
|
|
||||||
pause,
|
|
||||||
}}
|
|
||||||
attributes={$audio_attributes}
|
|
||||||
/>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { default as AudioPlayer } from './audio/player.svelte';
|
|
||||||
|
|
||||||
export * from './audio/actions';
|
|
||||||
export * from './user';
|
|
||||||
|
|
||||||
export { AudioPlayer };
|
|
||||||
export default AudioPlayer;
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export { default as AudioPlayer, default as default } from './Player.svelte';
|
||||||
|
export { default as AudioProgress } from './Progress.svelte';
|
||||||
|
export * from './actions';
|
||||||
|
export * from './attributes';
|
||||||
|
export * from './metadata';
|
||||||
|
export * from './user-preferences';
|
||||||
|
export * from './user-progress';
|
||||||
|
export * from './utility/format-seconds';
|
||||||
|
|
||||||
|
// export type { AudioMetadata } from './audio-metadata';
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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),
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility functions module.
|
|
||||||
* @module internal
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './announce';
|
|
||||||
export * from './use-url';
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 };
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* An object representing the metadata for an audio source.
|
||||||
|
*/
|
||||||
|
export interface AudioMetadata {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import clamp from 'just-clamp';
|
||||||
|
import { persisted } from 'svelte-local-storage-store';
|
||||||
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
playback_rate: number;
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default user preferences
|
||||||
|
*/
|
||||||
|
const DEFAULT_USER_PREFERENCES: UserPreferences = {
|
||||||
|
playback_rate: 1,
|
||||||
|
volume: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user preferences store
|
||||||
|
*/
|
||||||
|
export const USER_PREFERENCES_STORE = persisted<UserPreferences>(
|
||||||
|
'USER_PREFERENCE',
|
||||||
|
DEFAULT_USER_PREFERENCES,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the playback rate for the user
|
||||||
|
* @param value - The playback rate value
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user preferences object
|
||||||
|
*/
|
||||||
|
export const user_preferences = {
|
||||||
|
subscribe: USER_PREFERENCES_STORE.subscribe,
|
||||||
|
set_playback_rate,
|
||||||
|
set_volume,
|
||||||
|
clear: clear_store,
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { persisted } from 'svelte-local-storage-store';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { announce } from './_internal_/announce';
|
||||||
|
import { audio_element } from './_internal_/audio-element';
|
||||||
|
import { use_url } from './_internal_/safe-use-url';
|
||||||
|
|
||||||
|
export interface UserProgress {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_USER_PROGRESS: UserProgress = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User progress store
|
||||||
|
*/
|
||||||
|
const USER_PROGRESS_STORE = persisted<UserProgress>(
|
||||||
|
'USER_PROGRESS',
|
||||||
|
DEFAULT_USER_PROGRESS,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves user progress
|
||||||
|
*/
|
||||||
|
const save_user_progress = () => {
|
||||||
|
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 = audio.currentTime;
|
||||||
|
USER_PROGRESS_STORE.update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[pathname]: current_time,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user progress for a given audio source
|
||||||
|
* @param src - Audio source
|
||||||
|
*/
|
||||||
|
const get_user_progress = (src: string) => {
|
||||||
|
const pathname = use_url(src).pathname;
|
||||||
|
|
||||||
|
const store: UserProgress = get(USER_PROGRESS_STORE);
|
||||||
|
|
||||||
|
const value = store[pathname];
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears user progress store
|
||||||
|
*/
|
||||||
|
const clear_user_progress = () =>
|
||||||
|
USER_PROGRESS_STORE.set(DEFAULT_USER_PROGRESS);
|
||||||
|
|
||||||
|
export const user_progress = {
|
||||||
|
subscribe: USER_PROGRESS_STORE.subscribe,
|
||||||
|
get: get_user_progress,
|
||||||
|
clear: clear_user_progress,
|
||||||
|
save: save_user_progress,
|
||||||
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* User storage module.
|
|
||||||
* @module user
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './user-preferences';
|
|
||||||
export * from './user-progress';
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { persisted } from 'svelte-local-storage-store';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { audio_element } from '../audio';
|
|
||||||
import { announce, use_url } from '../internal';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(audio_element);
|
|
||||||
if (!audio?.src) return;
|
|
||||||
|
|
||||||
announce.info('saving progress: ', audio.src);
|
|
||||||
|
|
||||||
const pathname = use_url(audio.src).pathname;
|
|
||||||
const current_time = audio.currentTime;
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { announce } from '../_internal_/announce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 force_hours - Whether to always include hours in the timestamp, even if it's 0.
|
||||||
|
* @param seperator - The seperator to use between the hours, minutes, and seconds.
|
||||||
|
* @returns A string timestamp in the format of "hh:mm:ss".
|
||||||
|
*/
|
||||||
|
function seconds_to_timestamp(
|
||||||
|
seconds: number,
|
||||||
|
force_hours = false,
|
||||||
|
seperator = ':',
|
||||||
|
): string {
|
||||||
|
const safe_seconds = Number(seconds) || 0;
|
||||||
|
|
||||||
|
announce.info({ seconds, force_hours, seperator, safe_seconds });
|
||||||
|
|
||||||
|
const hours = Math.floor(safe_seconds / 3600);
|
||||||
|
const hours_remainder = safe_seconds % 3600;
|
||||||
|
|
||||||
|
const minutes = Math.floor(hours_remainder / 60);
|
||||||
|
const minutes_remainder = hours_remainder % 60;
|
||||||
|
|
||||||
|
const secs = Math.floor(minutes_remainder);
|
||||||
|
|
||||||
|
const show_hours = force_hours || hours > 0;
|
||||||
|
|
||||||
|
const HH = hours.toString().padStart(2, '0');
|
||||||
|
const MM = minutes.toString().padStart(2, '0');
|
||||||
|
const SS = secs.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const values = show_hours ? [HH, MM, SS] : [MM, SS];
|
||||||
|
|
||||||
|
return values.join(seperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const format_seconds = Object.freeze({
|
||||||
|
to_timestamp: seconds_to_timestamp,
|
||||||
|
});
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility functions module.
|
|
||||||
* @module utility
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './seconds-to-timestamp';
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* Returns a string timestamp in the format of "hh:mm:ss" from a given number of seconds.
|
|
||||||
* @param {number} seconds - The number of seconds to convert to a timestamp.
|
|
||||||
* @param {boolean} [force_hours=false] - Whether to always include hours in the timestamp, even if it's 0.
|
|
||||||
* @returns {string} A string timestamp in the format of "hh:mm:ss".
|
|
||||||
*/
|
|
||||||
export function seconds_to_timestamp(seconds, force_hours = false) {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const hours_remainder = seconds % 3600;
|
|
||||||
|
|
||||||
const minutes = Math.floor(hours_remainder / 60);
|
|
||||||
const minutes_remainder = hours_remainder % 60;
|
|
||||||
|
|
||||||
const secs = Math.floor(minutes_remainder);
|
|
||||||
|
|
||||||
const hours_str = force_hours || hours > 0 ? `${section(hours)}:` : '';
|
|
||||||
const minutes_str = minutes ? `${section(minutes)}:` : '00:';
|
|
||||||
const seconds_str = secs ? `${section(secs)}` : '00';
|
|
||||||
|
|
||||||
return `${hours_str}${minutes_str}${seconds_str}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string representation of a number with a leading 0 if it's less than 10.
|
|
||||||
* @param {number} num - The number to format.
|
|
||||||
* @returns {string} A string representation of the number with a leading 0 if it's less than 10.
|
|
||||||
*/
|
|
||||||
function section(num) {
|
|
||||||
if (num < 10) return `0${num}`;
|
|
||||||
return num.toString();
|
|
||||||
}
|
|
||||||
@@ -51,12 +51,9 @@
|
|||||||
{ label: 'audio', anchor: 'audio' },
|
{ label: 'audio', anchor: 'audio' },
|
||||||
{ label: 'audio_data', anchor: 'audio_data' },
|
{ label: 'audio_data', anchor: 'audio_data' },
|
||||||
{ label: 'audio.subscribe', anchor: 'audio.subscribe' },
|
{ label: 'audio.subscribe', anchor: 'audio.subscribe' },
|
||||||
{ label: 'audio.load', anchor: 'audio.load' },
|
{ label: 'audio.src', anchor: 'audio.src' },
|
||||||
{ label: 'audio.unload', anchor: 'audio.unload' },
|
|
||||||
{ label: 'audio.play', anchor: 'audio.play' },
|
{ label: 'audio.play', anchor: 'audio.play' },
|
||||||
{ label: 'audio.pause', anchor: 'audio.pause' },
|
|
||||||
{ label: 'audio.mute', anchor: 'audio.mute' },
|
{ label: 'audio.mute', anchor: 'audio.mute' },
|
||||||
{ label: 'audio.unmute', anchor: 'audio.unmute' },
|
|
||||||
{ label: 'audio.seek', anchor: 'audio.seek' },
|
{ label: 'audio.seek', anchor: 'audio.seek' },
|
||||||
{ label: 'audio.skip', anchor: 'audio.skip' },
|
{ label: 'audio.skip', anchor: 'audio.skip' },
|
||||||
{ label: 'user_preferences', anchor: 'user_preferences' },
|
{ label: 'user_preferences', anchor: 'user_preferences' },
|
||||||
@@ -115,6 +112,11 @@
|
|||||||
href: 'https://syntax.fm/',
|
href: 'https://syntax.fm/',
|
||||||
src: '/syntax.png',
|
src: '/syntax.png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'PodStack',
|
||||||
|
href: 'https://podstack.club/',
|
||||||
|
src: '/podstack.png',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let is_menu_open = false;
|
let is_menu_open = false;
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
import { PlayerWidget } from '$src/components/player';
|
import { PlayerWidget } from '$src/components/player';
|
||||||
import { episodes } from '$src/content/episodes';
|
import { episodes } from '$src/content/episodes';
|
||||||
import { DocsPage } from '$src/layout/page';
|
import { DocsPage } from '$src/layout/page';
|
||||||
|
import { audio } from '../lib/actions';
|
||||||
|
|
||||||
/** @type { string | undefined} */
|
audio.src.load(episodes.syntax.src, {
|
||||||
let audio_src = episodes.knomii.src;
|
title: episodes.syntax.title,
|
||||||
|
artwork: episodes.syntax.artwork,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DocsPage title="Docs" let:Section>
|
<DocsPage title="Docs" let:Section>
|
||||||
@@ -87,7 +90,6 @@
|
|||||||
class="-m-2 grid grid-cols-1 grid-rows-1 rounded-xl border border-mono-100 bg-white p-1 shadow-2xl shadow-mono-200 sm:p-2 lg:-m-4 lg:rounded-2xl lg:p-4"
|
class="-m-2 grid grid-cols-1 grid-rows-1 rounded-xl border border-mono-100 bg-white p-1 shadow-2xl shadow-mono-200 sm:p-2 lg:-m-4 lg:rounded-2xl lg:p-4"
|
||||||
>
|
>
|
||||||
<PlayerWidget
|
<PlayerWidget
|
||||||
src={audio_src}
|
|
||||||
class="w-full border border-mono-100"
|
class="w-full border border-mono-100"
|
||||||
options={{
|
options={{
|
||||||
duration: true,
|
duration: true,
|
||||||
|
|||||||
@@ -5,13 +5,10 @@
|
|||||||
import {
|
import {
|
||||||
audio_load,
|
audio_load,
|
||||||
audio_mute,
|
audio_mute,
|
||||||
audio_pause,
|
|
||||||
audio_play,
|
audio_play,
|
||||||
audio_seek,
|
audio_seek,
|
||||||
audio_skip,
|
audio_skip,
|
||||||
audio_subscribe,
|
audio_subscribe,
|
||||||
audio_unload,
|
|
||||||
audio_unmute,
|
|
||||||
seconds_to_timestamp,
|
seconds_to_timestamp,
|
||||||
user_preferences_clear,
|
user_preferences_clear,
|
||||||
user_preferences_set_playback_rate,
|
user_preferences_set_playback_rate,
|
||||||
@@ -87,23 +84,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</SectionArticle>
|
</SectionArticle>
|
||||||
|
|
||||||
<SectionArticle title="audio.load">
|
<SectionArticle title="audio.src">
|
||||||
<p>
|
<p>
|
||||||
Load a new audio source. This will stop the current audio source and
|
Load a new audio source. This will stop the current audio source and
|
||||||
replace it with the new one.
|
replace it with the new one.
|
||||||
</p>
|
</p>
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={audio_load} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
</SectionArticle>
|
|
||||||
|
|
||||||
<SectionArticle title="audio.unload">
|
|
||||||
<p>
|
<p>
|
||||||
Unload the current audio source. This will stop the current audio
|
Alternatively, unload the current audio source. This will stop the
|
||||||
source and remove it from the audio state.
|
current audio source and remove it from the audio state.
|
||||||
</p>
|
</p>
|
||||||
<div class="not-prose codeblock">
|
<div class="not-prose codeblock">
|
||||||
<Highlight code={audio_unload} language={lang_ts} />
|
<Highlight code={audio_load} language={lang_ts} />
|
||||||
</div>
|
</div>
|
||||||
</SectionArticle>
|
</SectionArticle>
|
||||||
|
|
||||||
@@ -114,13 +105,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</SectionArticle>
|
</SectionArticle>
|
||||||
|
|
||||||
<SectionArticle title="audio.pause">
|
|
||||||
<p>Set or toggle the pause state.</p>
|
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={audio_pause} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
</SectionArticle>
|
|
||||||
|
|
||||||
<SectionArticle title="audio.mute">
|
<SectionArticle title="audio.mute">
|
||||||
<p>Set or toggle the mute state.</p>
|
<p>Set or toggle the mute state.</p>
|
||||||
<div class="not-prose codeblock">
|
<div class="not-prose codeblock">
|
||||||
@@ -128,13 +112,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</SectionArticle>
|
</SectionArticle>
|
||||||
|
|
||||||
<SectionArticle title="audio.unmute">
|
|
||||||
<p>Set or toggle the unmute state.</p>
|
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={audio_unmute} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
</SectionArticle>
|
|
||||||
|
|
||||||
<SectionArticle title="audio.seek">
|
<SectionArticle title="audio.seek">
|
||||||
<p>
|
<p>
|
||||||
Seek to a specific time in the audio. The `from` parameter
|
Seek to a specific time in the audio. The `from` parameter
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
// load a new audio source
|
// load a new audio source into the player
|
||||||
audio.load(
|
audio.src.load(
|
||||||
// path to audio file
|
// path to audio file
|
||||||
'/episode-377.mp3',
|
'/episode-377.mp3',
|
||||||
|
|
||||||
// custom metadata
|
// custom metadata defined by you
|
||||||
{
|
{
|
||||||
title: 'Supper Club × Rich Harris, Author of Svelte',
|
title: 'Supper Club × Rich Harris, Author of Svelte',
|
||||||
|
artwork:
|
||||||
|
'https://ssl-static.libsyn.com/p/assets/b/3/c/d/b3cdf28da11ad39fe5bbc093207a2619/Syntax_-_499.jpg',
|
||||||
guests: ['Rich Harris'],
|
guests: ['Rich Harris'],
|
||||||
hosts: ['Wes Bos', 'Scott Tolinski'],
|
hosts: ['Wes Bos', 'Scott Tolinski'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// autoplay
|
// autoplay
|
||||||
true,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// unload the current audio source
|
||||||
|
audio.src.unload();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
// mute the current audio source
|
// mute the current audio source
|
||||||
// do nothing if it's already muted
|
audio.mute(true);
|
||||||
audio.mute('set');
|
|
||||||
|
|
||||||
// mute the current audio if it's unmuted
|
// unmute the current audio source
|
||||||
// unmute the current audio if it's muted
|
audio.mute(false);
|
||||||
|
|
||||||
|
// invert the mute state of the current audio source
|
||||||
audio.mute('toggle');
|
audio.mute('toggle');
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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');
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
// play the current audio source
|
// play the current audio source
|
||||||
// do nothing if it's already playing
|
audio.play(true);
|
||||||
audio.play('set');
|
|
||||||
|
|
||||||
// play the current audio if it's paused
|
// pause the current audio source
|
||||||
// pause the current audio if it's playing
|
audio.play(false);
|
||||||
|
|
||||||
|
// invert the play state of the current audio source
|
||||||
audio.play('toggle');
|
audio.play('toggle');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
// go to 200 seconds from the start
|
// go to 200 seconds from the start
|
||||||
audio.seek(200);
|
audio.seek_to(200);
|
||||||
|
|
||||||
// go to 200 seconds before the end
|
// go to 200 seconds before the end
|
||||||
audio.seek(200, 'from-end');
|
audio.seek_to(200, 'from-end');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
// skip forward 30 seconds from the current position
|
// skip forward 30 seconds from the current position
|
||||||
audio.skip(30);
|
audio.skip_by(30);
|
||||||
|
|
||||||
// skip backward 30 seconds from the current position
|
// skip backward 30 seconds from the current position
|
||||||
audio.skip(30, 'backward');
|
audio.skip_by(30, 'backward');
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
|
||||||
|
|
||||||
// unload the current audio source
|
|
||||||
audio.unload();
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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');
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { format_code } from '../../../layout/helper';
|
import { format_code } from '../../../layout/helper';
|
||||||
import { default as audio_load_src } from './audio-load.js?raw';
|
import { default as audio_load_src } from './audio-load?raw';
|
||||||
import { default as audio_mute_src } from './audio-mute.js?raw';
|
import { default as audio_mute_src } from './audio-mute?raw';
|
||||||
import { default as audio_pause_src } from './audio-pause.js?raw';
|
import { default as audio_play_src } from './audio-play?raw';
|
||||||
import { default as audio_play_src } from './audio-play.js?raw';
|
import { default as audio_seek_src } from './audio-seek?raw';
|
||||||
import { default as audio_seek_src } from './audio-seek.js?raw';
|
import { default as audio_skip_src } from './audio-skip?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_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_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_after_mount_src } from './load-audio-after-mount.svelte?raw';
|
||||||
import { default as seconds_to_timestamp_src } from './seconds-to-timestamp?raw';
|
import { default as seconds_to_timestamp_src } from './seconds-to-timestamp?raw';
|
||||||
@@ -25,12 +22,9 @@ export const load_audio_after_mount = format_code(load_audio_after_mount_src);
|
|||||||
export const seconds_to_timestamp = format_code(seconds_to_timestamp_src);
|
export const seconds_to_timestamp = format_code(seconds_to_timestamp_src);
|
||||||
export const audio_load = format_code(audio_load_src);
|
export const audio_load = format_code(audio_load_src);
|
||||||
export const audio_play = format_code(audio_play_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_skip = format_code(audio_skip_src);
|
||||||
export const audio_seek = format_code(audio_seek_src);
|
export const audio_seek = format_code(audio_seek_src);
|
||||||
export const audio_mute = format_code(audio_mute_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_clear = format_code(user_preferences_clear_src);
|
||||||
export const user_preferences_set_playback_rate = format_code(
|
export const user_preferences_set_playback_rate = format_code(
|
||||||
user_preferences_set_playback_rate_src,
|
user_preferences_set_playback_rate_src,
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
<!-- load the episode on click -->
|
<!-- load the episode on click -->
|
||||||
<button
|
<button
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
audio.load('/episode-audio.mp3', {
|
audio.src.load('/episode-audio.mp3', {
|
||||||
/* optional metadata */
|
/* your metadata */
|
||||||
|
title: 'Episode Title',
|
||||||
|
artwork: '/artwork.png',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Load Episode
|
Load Episode
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- unload the episode on click -->
|
<!-- unload the episode on click -->
|
||||||
<button on:click={() => audio.unload()}>Unload Episode</button>
|
<button on:click={() => audio.src.unload()}>Unload Episode</button>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// load the episode on mount without any metadata
|
// load the episode on mount without any metadata
|
||||||
audio.load('/episode-audio.mp3', {
|
audio.src.load('/episode-audio.mp3', {
|
||||||
/* optional metadata */
|
/* your metadata */
|
||||||
|
title: 'Episode Title',
|
||||||
|
artwork: '/artwork.png',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { seconds_to_timestamp } from 'svelte-podcast/utility';
|
import { format_seconds } from 'svelte-podcast/utility/format-seconds';
|
||||||
|
|
||||||
// Example
|
// Example
|
||||||
seconds_to_timestamp(5173); // 01:26:13
|
format_seconds.to_timestamp(5173); // '01:26:13';
|
||||||
|
|
||||||
// force hours
|
// force hours to be displayed
|
||||||
seconds_to_timestamp(2700, true); // 00:45:00
|
format_seconds.to_timestamp(2700, true); // '00:45:00';
|
||||||
seconds_to_timestamp(2700, false); // 45:00 (default)
|
|
||||||
|
// only display hours if there are any
|
||||||
|
format_seconds.to_timestamp(2700, false); // '45:00';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { user_preferences } from '../../../lib/user';
|
import { user_preferences } from 'svelte-podcast';
|
||||||
|
|
||||||
// Clears all user preferences
|
// Clears all user preferences
|
||||||
user_preferences.clear();
|
user_preferences.clear();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { user_preferences } from '../../../lib/user';
|
import { user_preferences } from 'svelte-podcast';
|
||||||
|
|
||||||
// Sets the playback speed to 150%
|
// Sets the playback speed to 150%
|
||||||
user_preferences.set_playback_rate(1.5);
|
user_preferences.set_playback_rate(1.5);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { user_preferences } from '../../../lib/user';
|
import { user_preferences } from 'svelte-podcast';
|
||||||
|
|
||||||
// Sets the volume to 75%
|
// Sets the volume to 75%
|
||||||
user_preferences.set_volume(0.75);
|
user_preferences.set_volume(0.75);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { AudioProgress } from 'svelte-podcast/audio';
|
import { AudioProgress } from 'svelte-podcast';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AudioProgress step={10} />
|
<AudioProgress step={10} />
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
Along side your audio source, you can optionally define arbitrary
|
Along side your audio source, you can optionally define arbitrary
|
||||||
metadata for you to use in your player, and determine if the episode
|
metadata for you to use in your player, and determine if the episode
|
||||||
should autoplay once loaded. Learn more in the <a
|
should autoplay once loaded. Learn more in the <a
|
||||||
href="/api/#audio.load">api docs</a
|
href="/api/#audio.src">api docs</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
audio.load('/episode-audio.mp3');
|
audio.src.load(
|
||||||
|
// Audio file
|
||||||
|
'/episode-audio.mp3',
|
||||||
|
|
||||||
audio.load(
|
// Your custom metadata
|
||||||
'https://media.transistor.fm/27a058c9/27b595e2.mp3',
|
|
||||||
{
|
{
|
||||||
title: 'SvelteKit-superforms with Andreas Söderlund',
|
title: 'A deep dive into Svelte Podcast',
|
||||||
artwork:
|
artwork: '/artwork.png',
|
||||||
'https://images.transistor.fm/file/transistor/images/show/12899/medium_1597678946-artwork.jpg',
|
guest_name: 'OllieJT',
|
||||||
guest_name: 'Andreas Söderlund',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Autoplay once loaded
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
audio.load('/episode-audio.mp3');
|
audio.src.load('/episode-audio.mp3', {
|
||||||
|
title: 'Episode Title',
|
||||||
|
artwork: '/artwork.png',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
import { audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
audio.load('https://media.transistor.fm/27a058c9/27b595e2.mp3');
|
audio.src.load('https://media.transistor.fm/27a058c9/27b595e2.mp3', {
|
||||||
|
title: 'Episode Title',
|
||||||
|
artwork: '/artwork.png',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// include this in your /src/app.d.ts file
|
// include this in your /src/app.d.ts file
|
||||||
|
|
||||||
declare module 'svelte-podcast' {
|
declare module 'svelte-podcast' {
|
||||||
interface EpisodeDetails {
|
interface AudioMetadata {
|
||||||
title: string;
|
title: string;
|
||||||
artwork: string;
|
artwork: string | null;
|
||||||
guest_name: string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Reference in New Issue
Block a user