Set state on load (#15)

This commit is contained in:
Ollie Taylor
2023-02-24 14:53:32 +00:00
committed by GitHub
19 changed files with 319 additions and 244 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'svelte-podcast': minor
---
- Restructures lib to make dist cleaner and dev easier to grock
- Refactors <audio /> bindings to make it easier to change values when loading a different source
- Moves more values to micro-stores
+2 -1
View File
@@ -3,6 +3,7 @@ node_modules
/build /build
/.svelte-kit /.svelte-kit
/package /package
/dist
.env .env
.env.* .env.*
!.env.example !.env.example
@@ -10,4 +11,4 @@ node_modules
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
yarn.lock yarn.lock
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts">
import {
audio_autoplay,
audio_current_time,
audio_element,
audio_metadata,
audio_muted,
audio_playback_rate,
audio_src,
audio_start_at,
audio_volume,
} from '$lib/context/audio-internals';
import type { PlayerElement } from '$lib/types/types';
import { isBoolean, isNumber } from '$pkg/type-guards';
let element: PlayerElement;
let currentTime = $audio_start_at;
let volume = $audio_volume;
let duration = 0;
let ended = true;
let muted = false;
let paused = false;
let playbackRate = 1;
// Update stores based on bindings
$: audio_element.set(element);
$: audio_current_time.set(currentTime);
$: audio_metadata.update((x) => ({ ...x, duration, ended, paused }));
// Update bindings based on stores
$: volume = $audio_volume;
$: muted = $audio_muted;
$: playbackRate = $audio_playback_rate;
$: currentTime = $audio_start_at;
</script>
<audio
bind:this={element}
src={$audio_src}
autoplay={$audio_autoplay}
bind:currentTime
bind:muted
bind:duration
bind:paused
bind:playbackRate
bind:volume
bind:ended
preload="metadata"
controls={false}
on:canplay={(e) => false && console.log('canplay', e)}
on:canplaythrough={(e) => false && console.log('canplaythrough', e)}
on:timeupdate={(e) => false && console.log('progress', e)}
on:loadedmetadata={(e) => false && console.log('loadedmetadata', e)}
on:suspend={(e) => false && console.log('suspend', e)}
on:seeked={(e) => false && console.log('seeked', e)}
on:play={(e) => false && console.log('play', e)}
on:waiting={(e) => console.log('waiting', e)}
on:stalled={(e) => console.log('stalled', e)}
on:load={(e) => console.log('load', e)}
on:emptied={() => {
// TODO: reset player state
// console.log('emptied', e);
}}
on:loadeddata={() => {
audio_metadata.update((m) => ({ ...m, loading: false }));
}}
on:loadstart={() => {
audio_metadata.update((m) => ({ ...m, loading: true }));
}}
on:playing={(e) => {
// @ts-expect-error event is not fully typed
const is_paused = e.target.paused;
if (!isBoolean(is_paused)) return;
audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:pause={(e) => {
// @ts-expect-error event is not fully typed
const is_paused = e.target.paused;
if (!isBoolean(is_paused)) return;
audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:ratechange={(e) => {
// @ts-expect-error event is not fully typed
const playbackRate = e.target.playbackRate;
if (!isNumber(playbackRate)) return;
audio_metadata.update((m) => ({ ...m, playbackRate }));
}}
on:durationchange={(e) => {
// @ts-expect-error event is not fully typed
const duration = e.target?.duration;
if (!isNumber(duration)) return;
audio_metadata.update((m) => ({ ...m, duration }));
}}
/>
-76
View File
@@ -1,76 +0,0 @@
<script lang="ts">
import {
__internal_audio_current_time,
__internal_audio_element,
__internal_audio_metadata,
__internal_audio_src,
} from '$lib/components/audio/store';
import type { PlayerElement } from '$lib/components/audio/types';
import { isBoolean, isNumber } from '$lib/helper/check';
let element: PlayerElement;
let currentTime = 0;
let muted = false;
$: src = $__internal_audio_src;
$: __internal_audio_element.set(element);
$: __internal_audio_current_time.set(currentTime);
$: __internal_audio_metadata.update((x) => ({ ...x, muted }));
</script>
{#key src}
<audio
bind:this={element}
{src}
autoplay={false}
bind:currentTime
bind:muted
preload="metadata"
controls={false}
on:canplay={(e) => false && console.log('canplay', e)}
on:canplaythrough={(e) => false && console.log('canplaythrough', e)}
on:timeupdate={(e) => false && console.log('progress', e)}
on:loadedmetadata={(e) => false && console.log('loadedmetadata', e)}
on:suspend={(e) => false && console.log('suspend', e)}
on:seeked={(e) => false && console.log('seeked', e)}
on:play={(e) => false && console.log('play', e)}
on:waiting={(e) => console.log('waiting', e)}
on:stalled={(e) => console.log('stalled', e)}
on:load={(e) => console.log('load', e)}
on:emptied={() => {
// TODO: reset player state
// console.log('emptied', e);
}}
on:loadeddata={() => {
__internal_audio_metadata.update((m) => ({ ...m, loading: false }));
}}
on:loadstart={() => {
__internal_audio_metadata.update((m) => ({ ...m, loading: true }));
}}
on:playing={(e) => {
// @ts-expect-error event is not fully typed
const is_paused = e.target.paused;
if (!isBoolean(is_paused)) return;
__internal_audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:pause={(e) => {
// @ts-expect-error event is not fully typed
const is_paused = e.target.paused;
if (!isBoolean(is_paused)) return;
__internal_audio_metadata.update((m) => ({ ...m, paused: is_paused }));
}}
on:ratechange={(e) => {
// @ts-expect-error event is not fully typed
const playbackRate = e.target.playbackRate;
if (!isNumber(playbackRate)) return;
__internal_audio_metadata.update((m) => ({ ...m, playbackRate }));
}}
on:durationchange={(e) => {
// @ts-expect-error event is not fully typed
const duration = e.target?.duration;
if (!isNumber(duration)) return;
__internal_audio_metadata.update((m) => ({ ...m, duration }));
}}
/>
<!-- <AudioA11yProgress {currentTime} {paused} {duration} /> -->
{/key}
-2
View File
@@ -1,2 +0,0 @@
export { default as AudioManager } from './audio.svelte';
export { audio } from './store';
-143
View File
@@ -1,143 +0,0 @@
import { dev } from '$app/environment';
import type { PlayerElement, PlayerMetadata } from '$lib/components/audio/types';
import { secondsToTimestamp } from '$lib/helper/seconds-to-timestamp';
import clamp from 'just-clamp';
import { derived, writable } from 'svelte/store';
function log(...val: (string | number | boolean)[]) {
if (!dev) return;
console.info('🎶 ', ...val);
}
function warn(...val: (string | number | boolean)[]) {
if (!dev) return;
console.warn('🎶 ', ...val);
}
export const __internal_audio_current_time = writable<number>(0);
export const __internal_audio_src = writable<string | null>(null);
export const __internal_audio_element = writable<PlayerElement>();
export const __internal_audio_metadata = writable<PlayerMetadata>({
duration: 0,
muted: false,
paused: true,
playbackRate: 1,
loading: false,
});
const state = derived(
[__internal_audio_metadata, __internal_audio_current_time],
([$metadata, $current_time]) => {
return {
current_time: $current_time,
timestamp: secondsToTimestamp($current_time),
...$metadata,
};
},
);
type HandleType = 'toggle' | 'set';
function play(type: HandleType = 'set') {
log('play: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.play();
}
})();
}
function pause(type: HandleType = 'set') {
log('pause: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.pause();
}
})();
}
function mute(type: HandleType = 'set') {
log('mute: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.muted = !el.muted;
} else {
el.muted = true;
}
})();
}
function unmute(type: HandleType = 'set') {
log('unmute: ', type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.muted = !el.muted;
} else {
el.muted = false;
}
})();
}
function load(src: string) {
log('load: ', src);
__internal_audio_src.set(src);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
el.volume = 1;
el.pause();
})();
}
function unload() {
log('unload: ');
__internal_audio_src.set(null);
}
function setPlaybackRate(rate: number) {
log('setPlaybackRate: ', rate);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
el.playbackRate = clamp(0, 10, rate);
})();
}
function seek(seconds: number, from: 'from-start' | 'from-end' = 'from-start') {
log('seek: ', seconds, from);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (from === 'from-end') {
el.currentTime = clamp(0, el.duration, el.duration - seconds);
} else {
el.currentTime = clamp(0, el.duration, seconds);
}
})();
}
function skip(seconds: number, type: 'forward' | 'backward' = 'forward') {
log('skip: ', seconds, type);
return __internal_audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'backward') {
el.currentTime = clamp(0, el.duration, el.currentTime - seconds);
} else {
el.currentTime = clamp(0, el.duration, el.currentTime + seconds);
}
})();
}
export const audio = {
subscribe: state.subscribe,
play,
pause,
mute,
unmute,
load,
unload,
setPlaybackRate,
seek,
skip,
};
-9
View File
@@ -1,9 +0,0 @@
export type PlayerElement = HTMLAudioElement | undefined;
export type PlayerMetadata = {
duration: number;
muted: boolean;
paused: boolean;
playbackRate: number;
loading: boolean;
};
+30
View File
@@ -0,0 +1,30 @@
import type { PlayerElement } from '$lib/types/types';
import { writable } from 'svelte/store';
export const audio_element = writable<PlayerElement>();
export const audio_start_at = writable<number>(0);
export const audio_autoplay = writable<boolean>(false);
export const audio_current_time = writable<number>(0);
export const audio_muted = writable<boolean>(false);
export const audio_playback_rate = writable<number>(1);
export const audio_src = writable<string>('');
export const audio_volume = writable<number>(1);
export type PlayerMetadata = {
duration: number;
// muted: boolean;
paused: boolean;
// playbackRate: number;
loading: boolean;
ended: boolean;
};
export const audio_metadata = writable<PlayerMetadata>({
duration: 0,
ended: false,
loading: false,
paused: true,
// muted: false,
// playbackRate: 1,
});
+146
View File
@@ -0,0 +1,146 @@
import {
audio_autoplay,
audio_current_time,
audio_element,
audio_metadata,
audio_muted,
audio_playback_rate,
audio_src,
audio_start_at,
audio_volume,
} from '$lib/context/audio-internals';
import { secondsToTimestamp } from '$lib/utility/seconds-to-timestamp';
import { info, warn } from '$pkg/log';
import clamp from 'just-clamp';
import { derived } from 'svelte/store';
const audio_state = derived(
[
audio_metadata,
audio_autoplay,
audio_current_time,
audio_muted,
audio_playback_rate,
audio_src,
audio_volume,
],
([$metadata, $autoplay, $current_time, $muted, $playback_rate, $src, $volume]) => {
return {
...$metadata,
autoplay: $autoplay,
current_time: $current_time,
timestamp: secondsToTimestamp($current_time),
muted: $muted,
playback_rate: $playback_rate,
src: $src,
volume: $volume,
};
},
);
type HandleType = 'toggle' | 'set';
function play(type: HandleType = 'set') {
info('play: ', type);
return audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.play();
}
})();
}
function pause(type: HandleType = 'set') {
info('pause: ', type);
return audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'toggle') {
el.paused ? el.play() : el.pause();
} else {
el.pause();
}
})();
}
function mute(type: HandleType = 'set') {
info('mute: ', type);
if (type === 'toggle') {
audio_muted.update((muted) => !muted);
} else {
audio_muted.set(true);
}
}
function unmute(type: HandleType = 'set') {
info('unmute: ', type);
if (type === 'toggle') {
audio_muted.update((muted) => !muted);
} else {
audio_muted.set(false);
}
}
type LoadOptions = {
autoplay: boolean;
start_at?: number;
};
function load(src: string, opts: LoadOptions) {
info('load: ', src);
audio_autoplay.set(opts.autoplay);
audio_volume.set(1);
audio_src.set(src);
audio_start_at.set(opts.start_at || 0);
// opts.current_time ? seek(opts.current_time) : null;
//opts.autoplay && play();
}
function unload() {
info('unload: ');
pause();
audio_autoplay.set(false);
audio_volume.set(1);
audio_src.set('');
audio_start_at.set(0);
}
function setPlaybackRate(rate: number) {
info('setPlaybackRate: ', rate);
audio_playback_rate.set(clamp(0, 10, rate));
}
function seek(seconds: number, from: 'from-start' | 'from-end' = 'from-start') {
info('seek: ', seconds, from);
return audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (from === 'from-end') {
el.currentTime = clamp(0, el.duration, el.duration - seconds);
} else {
el.currentTime = clamp(0, el.duration, seconds);
}
})();
}
function skip(seconds: number, type: 'forward' | 'backward' = 'forward') {
info('skip: ', seconds, type);
return audio_element.subscribe((el) => {
if (!el) return warn('no audio element');
if (type === 'backward') {
el.currentTime = clamp(0, el.duration, el.currentTime - seconds);
} else {
el.currentTime = clamp(0, el.duration, el.currentTime + seconds);
}
})();
}
export const audio = {
subscribe: audio_state.subscribe,
play,
pause,
mute,
unmute,
load,
unload,
setPlaybackRate,
seek,
skip,
};
+2 -2
View File
@@ -1,2 +1,2 @@
// Reexport your entry components here export { default as AudioLoader } from '$lib/components/audio-loader.svelte';
export * from '$lib/components/audio'; export { audio } from '$lib/context/audio';
+1
View File
@@ -0,0 +1 @@
export type PlayerElement = HTMLAudioElement | undefined;
+21
View File
@@ -0,0 +1,21 @@
import { dev } from '$app/environment';
const useLogger = {
error: console.error,
info: console.info,
warn: console.warn,
};
export type Logger = keyof typeof useLogger;
export function log(type: Logger, ...content: unknown[]) {
const logger = useLogger[type];
if (type === 'info' && !dev) return;
logger('🔊 svelte-podcast: ', content);
}
export const info = (...content: unknown[]) => log('info', ...content);
export const warn = (...content: unknown[]) => log('warn', content);
export const error = (...content: unknown[]) => log('error', content);
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AudioManager } from '$lib/components/audio'; import { AudioLoader } from '$lib';
</script> </script>
<AudioManager /> <AudioLoader />
<slot /> <slot />
+8 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { audio } from '$lib/components/audio'; import { audio } from '$lib';
const sources = { const sources = {
syntax: '/example-syntax.mp3', syntax: '/example-syntax.mp3',
@@ -16,8 +16,13 @@
<a href="/another-page">Another Page</a> <a href="/another-page">Another Page</a>
<h5>Load Audio</h5> <h5>Load Audio</h5>
<button type="button" on:click={() => audio.load(sources['syntax'])}>Syntax</button> <button
<button type="button" on:click={() => audio.load(sources['knomii'])}>Knomii</button> type="button"
on:click={() => audio.load(sources['syntax'], { autoplay: true, start_at: 45 })}>Syntax</button
>
<button type="button" on:click={() => audio.load(sources['knomii'], { autoplay: false })}
>Knomii</button
>
<button type="button" on:click={() => audio.unload()}>None</button> <button type="button" on:click={() => audio.unload()}>None</button>
<h5>Custom audio controls</h5> <h5>Custom audio controls</h5>
+1 -6
View File
@@ -1,10 +1,5 @@
<script lang="ts"> <script lang="ts">
import { audio } from '$lib/components/audio'; import { audio } from '$lib';
const sources = {
syntax: '/example-syntax.mp3',
knomii: '/example-knomii.mp3',
} as const;
let current_time = 0; let current_time = 0;
$: current_time = $audio.current_time; $: current_time = $audio.current_time;
+4
View File
@@ -9,6 +9,10 @@ const config = {
kit: { kit: {
adapter: adapter(), adapter: adapter(),
alias: {
$lib: 'src/lib',
$pkg: 'src/lib/utility/package/',
},
}, },
}; };