Simplify setup (#46)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte-podcast': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
refactor audio element logic
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte-podcast': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
rewrite docs with code samples
|
||||||
Vendored
+9
-1
@@ -10,6 +10,11 @@
|
|||||||
"source.organizeImports": true
|
"source.organizeImports": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
@@ -33,5 +38,8 @@
|
|||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/.svelte-kit": true
|
"**/.svelte-kit": true
|
||||||
},
|
},
|
||||||
"problems.sortOrder": "severity"
|
"problems.sortOrder": "severity",
|
||||||
|
"javascript.preferences.importModuleSpecifier": "relative",
|
||||||
|
"typescript.preferences.importModuleSpecifier": "relative",
|
||||||
|
"todo-tree.tree.scanMode": "workspace only"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -22,7 +22,7 @@
|
|||||||
.richtext h4 a,
|
.richtext h4 a,
|
||||||
.richtext h5 a,
|
.richtext h5 a,
|
||||||
.richtext h6 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 {
|
&::before {
|
||||||
@apply absolute -left-[1.25ch] inline-block font-light leading-normal text-mono-400;
|
@apply absolute -left-[1.25ch] inline-block font-light leading-normal text-mono-400;
|
||||||
@@ -37,16 +37,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.richtext code.hljs {
|
.richtext code.hljs {
|
||||||
@apply p-0 text-sm;
|
@apply overflow-x-auto p-0 text-sm;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.richtext .codeblock code.hljs {
|
.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 {
|
.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,
|
.richtext td,
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { default as PlayerStack } from './player-stack.svelte';
|
|
||||||
export { default as PlayerWidget } from './player-widget.svelte';
|
export { default as PlayerWidget } from './player-widget.svelte';
|
||||||
|
|||||||
@@ -1,444 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {
|
|
||||||
Pause,
|
|
||||||
Play,
|
|
||||||
SpeakerWave,
|
|
||||||
} from '@inqling/svelte-icons/heroicon-20-solid';
|
|
||||||
import {
|
|
||||||
AudioPlayer,
|
|
||||||
episode_progress,
|
|
||||||
user_preferences,
|
|
||||||
} from 'svelte-podcast';
|
|
||||||
import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
|
|
||||||
|
|
||||||
/** @type {string | undefined} */
|
|
||||||
export let src;
|
|
||||||
|
|
||||||
/** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
|
|
||||||
export let metadata = {};
|
|
||||||
|
|
||||||
export let hide_timestamps = false;
|
|
||||||
export let hide_playback_rate = false;
|
|
||||||
|
|
||||||
export let skip_back = 30;
|
|
||||||
export let skip_forward = 10;
|
|
||||||
|
|
||||||
export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AudioPlayer {src} {metadata} let:Player let:action let:episode>
|
|
||||||
<div
|
|
||||||
{...$$restProps}
|
|
||||||
class="svpod--container svpod--reset"
|
|
||||||
data-loaded={episode.is_loaded ? 'true' : 'false'}
|
|
||||||
>
|
|
||||||
<div class="svpod--container--row svpod--row--controls">
|
|
||||||
<button
|
|
||||||
on:click={() => action.skip_back(10)}
|
|
||||||
class="svpod--reset svpod--toggle-pause"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Skip value={skip_back} type="backward" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- toggle play -->
|
|
||||||
{#if episode.is_loaded}
|
|
||||||
<button
|
|
||||||
on:click={() => action.toggle()}
|
|
||||||
class="svpod--reset svpod--toggle-pause"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<A11yIcon
|
|
||||||
icon={episode.is_playing ? Pause : Play}
|
|
||||||
label={episode.is_playing ? 'Pause' : 'Play'}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div class="svpod--reset svpod--toggle-pause">
|
|
||||||
<A11yIcon icon={Spinner} label="Waiting for audio..." />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button
|
|
||||||
on:click={() => action.skip_forward(10)}
|
|
||||||
class="svpod--reset svpod--toggle-pause"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Skip value={skip_forward} type="forward" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="svpod--artwork svpod--row--artwork">
|
|
||||||
<span class="svpod--aspect--square" />
|
|
||||||
<slot>
|
|
||||||
<div class="svpod--artwork--placeholder">
|
|
||||||
<span><SpeakerWave style="width:1em; height: 1em;" /></span>
|
|
||||||
</div>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="svpod--timeline svpod--row--timeline">
|
|
||||||
<Player.AudioProgress />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !hide_timestamps || !hide_playback_rate}
|
|
||||||
<div class="svpod--container--row svpod--row--timestamps">
|
|
||||||
{#if !hide_timestamps}
|
|
||||||
<div class="svpod--timestamp">
|
|
||||||
<Timestamp
|
|
||||||
value={$episode_progress?.current_time || 0}
|
|
||||||
force_hours={episode.timestamp_hours}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="flex-grow: 1" />
|
|
||||||
|
|
||||||
<div class="svpod--timestamp">
|
|
||||||
<Timestamp value={episode.duration || 0} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !hide_playback_rate}
|
|
||||||
<select
|
|
||||||
class="svpod--playback-rate"
|
|
||||||
value={$user_preferences.playback_rate}
|
|
||||||
on:change={(e) => {
|
|
||||||
const value = parseFloat(e.currentTarget.value);
|
|
||||||
user_preferences.set_playback_rate(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#each playback_rate_values as value}
|
|
||||||
<option {value}>
|
|
||||||
{#if Number.isInteger(value)}
|
|
||||||
{value}.0
|
|
||||||
{:else}
|
|
||||||
{value}
|
|
||||||
{/if}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</AudioPlayer>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.svpod--container {
|
|
||||||
--fg: var(--svpod--content--base);
|
|
||||||
--bg: var(--svpod--surface--darker);
|
|
||||||
|
|
||||||
--padding: 2px;
|
|
||||||
--radius: 0.75em;
|
|
||||||
--inner-radius: calc(var(--radius) - var(--padding));
|
|
||||||
|
|
||||||
--svpod--timeline-track--shape--height: 3em;
|
|
||||||
--svpod--timeline-track--shape--radius: var(--inner-radius);
|
|
||||||
--svpod--timeline-track--shape--border: 2px;
|
|
||||||
|
|
||||||
--svpod--timeline-thumb--shape--height: var(
|
|
||||||
--svpod--timeline-track--shape--height
|
|
||||||
);
|
|
||||||
--svpod--timeline-thumb--shape--width: 3px;
|
|
||||||
--svpod--timeline-thumb--shape--radius: 1px;
|
|
||||||
--svpod--timeline-thumb--shape--border: 0px;
|
|
||||||
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1em;
|
|
||||||
width: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 600;
|
|
||||||
padding: var(--padding);
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--container--row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
min-width: max-content;
|
|
||||||
border-radius: var(--inner-radius);
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
.svpod--container--row:last-child {
|
|
||||||
border-top: 1px solid var(--svpod--surface--base);
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--row--artwork {
|
|
||||||
order: 0;
|
|
||||||
}
|
|
||||||
.svpod--row--timeline {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
.svpod--row--controls {
|
|
||||||
order: 2;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.svpod--row--timestamps {
|
|
||||||
order: 3;
|
|
||||||
min-height: 32px;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.svpod--timeline {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: stretch;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
font-size: 1em;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--toggle-pause {
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
font-size: 1em;
|
|
||||||
height: 3em;
|
|
||||||
justify-content: center;
|
|
||||||
width: 3em;
|
|
||||||
border-radius: var(--inner-radius);
|
|
||||||
}
|
|
||||||
button.svpod--toggle-pause {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.svpod--toggle-pause :global(svg) {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.svpod--timestamp {
|
|
||||||
background-color: var(--bg);
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 1em;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: var(--inner-radius);
|
|
||||||
width: max-content;
|
|
||||||
flex-grow: 0;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.svpod--playback-rate {
|
|
||||||
background-color: var(--bg);
|
|
||||||
appearance: none;
|
|
||||||
padding: 0 0.75em;
|
|
||||||
color: var(--fg);
|
|
||||||
overflow: hidden;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
line-height: 1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 48px;
|
|
||||||
outline-color: var(--svpod--accent--lighter);
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: var(--inner-radius);
|
|
||||||
}
|
|
||||||
select.svpod--playback-rate,
|
|
||||||
select.svpod--playback-rate:hover,
|
|
||||||
select.svpod--playback-rate:focus {
|
|
||||||
border: none;
|
|
||||||
shadow: none;
|
|
||||||
}
|
|
||||||
select.svpod--playback-rate:focus {
|
|
||||||
--fg: var(--svpod--accent--lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.svpod--toggle-pause,
|
|
||||||
select.svpod--playback-rate {
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
button.svpod--toggle-pause:hover,
|
|
||||||
select.svpod--playback-rate:hover {
|
|
||||||
--bg: var(--svpod--surface--base);
|
|
||||||
--fg: var(--svpod--content--lighter);
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
div.svpod--artwork {
|
|
||||||
position: relative;
|
|
||||||
border-radius: var(--inner-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
div.svpod--artwork > :global(div),
|
|
||||||
div.svpod--artwork > :global(img) {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.svpod--artwork--placeholder {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--svpod--accent--lighter);
|
|
||||||
font-size: 2.5em;
|
|
||||||
}
|
|
||||||
.svpod--artwork--placeholder :global(svg) {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.svpod--artwork--placeholder::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
opacity: 0.5;
|
|
||||||
background: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
var(--svpod--accent--base),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
span.svpod--aspect--square {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
line-height: 0;
|
|
||||||
padding-bottom: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--timeline {
|
|
||||||
:global(input[type='range']) {
|
|
||||||
--track--shape--height: calc(
|
|
||||||
var(--svpod--timeline-track--shape--height)
|
|
||||||
);
|
|
||||||
--thumb-border-offset: calc(
|
|
||||||
var(--svpod--timeline-thumb--shape--border) * 2
|
|
||||||
);
|
|
||||||
--thumb--shape--height: calc(
|
|
||||||
var(--svpod--timeline-thumb--shape--height) -
|
|
||||||
var(--thumb-border-offset)
|
|
||||||
);
|
|
||||||
|
|
||||||
--svpod--timeline-track--bg: var(--svpod--surface--base);
|
|
||||||
--svpod--timeline-track--border: var(--bg);
|
|
||||||
|
|
||||||
--svpod--timeline-thumb--bg: var(--svpod--content--base);
|
|
||||||
--svpod--timeline-thumb--border: var(--bg);
|
|
||||||
|
|
||||||
font-size: 1em;
|
|
||||||
|
|
||||||
--borders: calc(var(--svpod--timeline-thumb--shape--border) * 2);
|
|
||||||
--no-shadow: 0px 0px 0px transparent;
|
|
||||||
|
|
||||||
height: calc(var(--thumb--shape--height) + var(--borders));
|
|
||||||
appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
:global(input[type='range']:hover) {
|
|
||||||
--svpod--timeline-track--bg: var(--svpod--accent--darker);
|
|
||||||
--svpod--timeline-track--border: var(--svpod--accent--base);
|
|
||||||
--svpod--timeline-thumb--bg: var(--svpod--accent--lighter);
|
|
||||||
}
|
|
||||||
:global(input[type='range']:focus) {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-webkit-slider-runnable-track) {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--track--shape--height);
|
|
||||||
cursor: pointer;
|
|
||||||
animate: 0.2s;
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
background: var(--svpod--timeline-track--bg);
|
|
||||||
border-radius: var(--svpod--timeline-track--shape--radius);
|
|
||||||
border: var(--svpod--timeline-track--shape--border) solid
|
|
||||||
var(--svpod--timeline-track--border);
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-webkit-slider-thumb) {
|
|
||||||
--offset: calc(var(--svpod--timeline-track--shape--border) * -1);
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
border: var(--svpod--timeline-thumb--shape--border) solid
|
|
||||||
var(--svpod--timeline-thumb--border);
|
|
||||||
height: var(--thumb--shape--height);
|
|
||||||
width: var(--svpod--timeline-thumb--shape--width);
|
|
||||||
border-radius: var(--svpod--timeline-thumb--shape--radius);
|
|
||||||
background: var(--svpod--timeline-thumb--bg);
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin-top: var(--offset);
|
|
||||||
}
|
|
||||||
:global(input[type='range']:focus::-webkit-slider-runnable-track) {
|
|
||||||
background: var(--svpod--timeline-track--bg);
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-moz-range-track) {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--track--shape--height);
|
|
||||||
cursor: pointer;
|
|
||||||
animate: 0.2s;
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
background: var(--svpod--timeline-track--bg);
|
|
||||||
border-radius: var(--svpod--timeline-track--shape--radius);
|
|
||||||
border: var(--svpod--timeline-track--shape--border) solid
|
|
||||||
var(--svpod--timeline-track--border);
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-moz-range-thumb) {
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
border: var(--svpod--timeline-thumb--shape--border) solid
|
|
||||||
var(--svpod--timeline-thumb--border);
|
|
||||||
height: var(--thumb--shape--height);
|
|
||||||
width: var(--svpod--timeline-thumb--shape--width);
|
|
||||||
border-radius: var(--svpod--timeline-thumb--shape--radius);
|
|
||||||
background: var(--svpod--timeline-thumb--bg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-ms-track) {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--track--shape--height);
|
|
||||||
cursor: pointer;
|
|
||||||
animate: 0.2s;
|
|
||||||
background: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-ms-fill-lower),
|
|
||||||
:global(input[type='range']::-ms-fill-upper) {
|
|
||||||
background: var(--svpod--timeline-track--bg);
|
|
||||||
border: var(--svpod--timeline-track--shape--border) solid
|
|
||||||
var(--svpod--timeline-track--border);
|
|
||||||
border-radius: calc(var(--svpod--timeline-track--shape--radius) * 2);
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
}
|
|
||||||
:global(input[type='range']::-ms-thumb) {
|
|
||||||
margin-top: 1px;
|
|
||||||
box-shadow: var(--no-shadow);
|
|
||||||
border: var(--svpod--timeline-thumb--shape--border) solid
|
|
||||||
var(--svpod--timeline-thumb--border);
|
|
||||||
height: var(--thumb--shape--height);
|
|
||||||
width: var(--svpod--timeline-thumb--shape--width);
|
|
||||||
border-radius: var(--svpod--timeline-thumb--shape--radius);
|
|
||||||
background: var(--svpod--timeline-thumb--bg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
:global(input[type='range']:focus::-ms-fill-lower),
|
|
||||||
:global(input[type='range']:focus::-ms-fill-upper) {
|
|
||||||
background: var(--svpod--timeline-track--bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
/** @type {string | undefined} */
|
/** @type {string | undefined} */
|
||||||
export let src;
|
export let src;
|
||||||
|
|
||||||
/** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
|
/** @type {import('svelte-podcast/audio').AudioMetadata} */
|
||||||
export let metadata = {};
|
export let metadata = {};
|
||||||
|
|
||||||
export let hide_duration = false;
|
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];
|
export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AudioPlayer {src} {metadata} let:Player let:action let:episode>
|
<svelte:head>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--svpod--font: ui-sans-serif, system-ui, -apple-system,
|
||||||
|
BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
|
||||||
|
--svpod--surface--darker: rgb(0, 0, 0);
|
||||||
|
--svpod--surface--base: rgb(40, 40, 40);
|
||||||
|
--svpod--surface--lighter: rgb(60, 60, 60);
|
||||||
|
|
||||||
|
--svpod--content--darker: rgb(150, 150, 150);
|
||||||
|
--svpod--content--base: rgb(200, 200, 200);
|
||||||
|
--svpod--content--lighter: rgb(255, 255, 255);
|
||||||
|
|
||||||
|
--svpod--accent--darker: rgb(75, 15, 0);
|
||||||
|
--svpod--accent--base: rgb(180, 40, 0);
|
||||||
|
--svpod--accent--lighter: rgb(255, 55, 25);
|
||||||
|
|
||||||
|
--bg: var(--svpod--surface--darker);
|
||||||
|
--fg: var(--svpod--content--darker);
|
||||||
|
|
||||||
|
--svpod--radius--rounded: 9999px;
|
||||||
|
|
||||||
|
/* element : timeline */
|
||||||
|
|
||||||
|
--svpod--timeline-track--shape--height: 20px;
|
||||||
|
--svpod--timeline-track--shape--radius: 0;
|
||||||
|
--svpod--timeline-track--shape--border: 0;
|
||||||
|
|
||||||
|
--svpod--timeline-thumb--shape--width: 8px;
|
||||||
|
--svpod--timeline-thumb--shape--height: 32px;
|
||||||
|
--svpod--timeline-thumb--shape--radius: 6px;
|
||||||
|
--svpod--timeline-thumb--shape--border: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svpod--reset {
|
||||||
|
text-transform: none;
|
||||||
|
font-style: normal;
|
||||||
|
text-indent: 0;
|
||||||
|
text-shadow: none;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svpod--a11y-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svpod--button,
|
||||||
|
.svpod--icon-button {
|
||||||
|
--fg: var(--svpod--content--base);
|
||||||
|
--bg: var(--svpod--surface--base);
|
||||||
|
position: relative;
|
||||||
|
line-height: 1em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 0px solid transparent;
|
||||||
|
border-radius: var(--svpod--radius--rounded);
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.svpod--button:hover,
|
||||||
|
.svpod--icon-button:hover {
|
||||||
|
--fg: var(--svpod--content--lighter);
|
||||||
|
--bg: var(--svpod--surface--lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svpod--button {
|
||||||
|
padding: 4px calc(1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svpod--icon-button {
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<AudioPlayer {src} {metadata} let:Player let:action let:attributes>
|
||||||
<div
|
<div
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
class="svpod--container svpod--reset"
|
class="svpod--container svpod--reset"
|
||||||
data-loaded={episode.is_loaded ? 'true' : 'false'}
|
data-loaded={attributes.is_loaded ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
{#if !hide_skip_back}
|
{#if !hide_skip_back}
|
||||||
<button
|
<button
|
||||||
@@ -38,15 +132,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- toggle play -->
|
<!-- toggle play -->
|
||||||
{#if episode.is_loaded}
|
{#if attributes.is_loaded}
|
||||||
<button
|
<button
|
||||||
on:click={() => action.toggle()}
|
on:click={() => action.toggle()}
|
||||||
class="svpod--reset svpod--toggle-pause"
|
class="svpod--reset svpod--toggle-pause"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<A11yIcon
|
<A11yIcon
|
||||||
icon={episode.is_playing ? Pause : Play}
|
icon={attributes.is_paused ? Play : Pause}
|
||||||
label={episode.is_playing ? 'Pause' : 'Play'}
|
label={attributes.is_paused ? 'Play' : 'Pause'}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -68,8 +162,8 @@
|
|||||||
{#if !hide_current_time}
|
{#if !hide_current_time}
|
||||||
<div class="svpod--timestamp">
|
<div class="svpod--timestamp">
|
||||||
<Timestamp
|
<Timestamp
|
||||||
value={episode.current_time}
|
value={attributes.current_time}
|
||||||
force_hours={episode.timestamp_hours}
|
force_hours={attributes.duration >= 3600}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -80,7 +174,7 @@
|
|||||||
|
|
||||||
{#if !hide_duration}
|
{#if !hide_duration}
|
||||||
<div class="svpod--timestamp">
|
<div class="svpod--timestamp">
|
||||||
<Timestamp value={episode.duration} />
|
<Timestamp value={attributes.duration} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -153,8 +153,4 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
/* animation: Spin 1s cubic-bezier(0.5, 0.1, 0.1, 0.8) infinite; */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { seconds_to_timestamp } from 'svelte-podcast';
|
import { seconds_to_timestamp } from 'svelte-podcast/utility';
|
||||||
|
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
export let value;
|
export let value;
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ export const episodes = Object.freeze({
|
|||||||
title: `Empowerment starts with letting go of control`,
|
title: `Empowerment starts with letting go of control`,
|
||||||
artwork: `https://ssl-static.libsyn.com/p/assets/f/a/8/d/fa8d56d5226884335f2e77a3093c12a1/ep-6.png`,
|
artwork: `https://ssl-static.libsyn.com/p/assets/f/a/8/d/fa8d56d5226884335f2e77a3093c12a1/ep-6.png`,
|
||||||
},
|
},
|
||||||
|
svelte: {
|
||||||
|
src: 'https://media.transistor.fm/6d6c49e4/4cecfd0d.mp3?src=site',
|
||||||
|
title: 'SvelteKit-superforms with Andreas Söderlund',
|
||||||
|
artwork:
|
||||||
|
'https://images.transistor.fm/file/transistor/images/show/12899/medium_1597678946-artwork.jpg',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
|
import { BROWSER } from 'esm-env';
|
||||||
|
|
||||||
export const slugify = (/** @type {string} */ str) =>
|
export const slugify = (/** @type {string} */ str) =>
|
||||||
encodeURI(str.toLowerCase().trim().replaceAll(' ', '_'));
|
encodeURI(str.toLowerCase().trim().replaceAll(' ', '_'));
|
||||||
|
|
||||||
export const format_code = (/** @type {string} */ code) =>
|
export const format_code = (/** @type {string} */ code) =>
|
||||||
code.replaceAll('\t', ' ');
|
code.replaceAll('\t', ' ');
|
||||||
|
|
||||||
|
export const on_this_page = () => {
|
||||||
|
if (!BROWSER) return [];
|
||||||
|
|
||||||
|
let anchorLinks = [];
|
||||||
|
let aTags = document.getElementsByTagName('a');
|
||||||
|
|
||||||
|
for (let i = 0; i < aTags.length; i++) {
|
||||||
|
let href = aTags[i]?.getAttribute('href');
|
||||||
|
if (href && href.startsWith('#')) {
|
||||||
|
anchorLinks.push(href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchorLinks;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
|
|
||||||
@@ -10,9 +9,8 @@
|
|||||||
export let description =
|
export let description =
|
||||||
'A suite of tools and components to build your own podcast players, and work with RSS podcast data in SvelteKit.';
|
'A suite of tools and components to build your own podcast players, and work with RSS podcast data in SvelteKit.';
|
||||||
|
|
||||||
// TODO: update canonical link with static link
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
export let canonical = $page.url.href + base;
|
export let canonical = 'https://svelte-podcast.com' + $page.url.pathname;
|
||||||
|
|
||||||
// TODO: add screenshots under images
|
// TODO: add screenshots under images
|
||||||
/** @type {import('svelte-meta-tags').OpenGraph["images"]}*/
|
/** @type {import('svelte-meta-tags').OpenGraph["images"]}*/
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
<script>
|
<script>
|
||||||
import { slugify } from '$src/layout/helper';
|
import { on_this_page, slugify } from '$src/layout/helper';
|
||||||
import Metadata from '$src/layout/metadata.svelte';
|
import Metadata from '$src/layout/metadata.svelte';
|
||||||
import Container from '$src/layout/page/container.svelte';
|
import Container from '$src/layout/page/container.svelte';
|
||||||
import SectionArticle from '$src/layout/page/section-article.svelte';
|
import SectionArticle from '$src/layout/page/section-article.svelte';
|
||||||
import Section from '$src/layout/page/section.svelte';
|
import Section from '$src/layout/page/section.svelte';
|
||||||
import TableModule from '$src/layout/page/table-module.svelte';
|
|
||||||
import TableSchema from '$src/layout/page/table-schema.svelte';
|
import TableSchema from '$src/layout/page/table-schema.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
/** @type {string}*/
|
/** @type {string}*/
|
||||||
export let title;
|
export let title;
|
||||||
|
|
||||||
export let anchor = slugify(title);
|
export let anchor = slugify(title);
|
||||||
|
|
||||||
|
const anchor_links = on_this_page();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('on_this_page', JSON.stringify(anchor_links, null, 3));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Metadata {title} />
|
<Metadata {title} />
|
||||||
|
|
||||||
<slot name="header" {anchor} {title}>
|
<slot name="header" {anchor} {title}>
|
||||||
<Container as="header" aria-labelledby={anchor}>
|
<Container as="header" aria-labelledby={anchor}>
|
||||||
<div class="richtext pt-20">
|
<div class="richtext sm:pt-10 lg:pt-20">
|
||||||
<h1 id={anchor} class="my-0">{title}</h1>
|
<h1 id={anchor} class="my-0">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<slot {Section} {SectionArticle} {TableModule} {TableSchema} />
|
<slot {Section} {SectionArticle} {TableSchema} />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<svelte:element
|
<svelte:element
|
||||||
this={as}
|
this={as}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
class="relative mx-auto w-full max-w-6xl p-4 md:p-6"
|
class="relative mx-auto w-full max-w-6xl px-2 py-4 md:px-6 md:py-6"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { Highlight } from 'svelte-highlight';
|
|
||||||
import lang_ts from 'svelte-highlight/languages/typescript';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table Row
|
|
||||||
* @typedef {Object} Row
|
|
||||||
* @property {string} method - Method on the module
|
|
||||||
* @property {string} description - Description of the method
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {Row[]} */
|
|
||||||
export let rows;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="w-full overflow-x-auto">
|
|
||||||
<table class="">
|
|
||||||
<thead>
|
|
||||||
<th class="w-1/2">Method</th>
|
|
||||||
<th class="w-1/2">Description</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each rows as { method, description }}
|
|
||||||
<tr>
|
|
||||||
<td class="not-prose">
|
|
||||||
<Highlight code={method} language={lang_ts} />
|
|
||||||
</td>
|
|
||||||
<td class=""> {description} </td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
export let rows;
|
export let rows;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="-mx-6 min-w-full overflow-x-auto bg-white md:-mx-8">
|
||||||
<table class="">
|
<table class="my-2 w-max min-w-full">
|
||||||
<thead>
|
<thead class="">
|
||||||
<th class="w-max">Property</th>
|
<th class="w-max">Property</th>
|
||||||
<th class="w-max">Type</th>
|
<th class="w-max">Type</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
|
|||||||
@@ -36,6 +36,16 @@
|
|||||||
/** @type {Readonly<ResourceLink[]>} */
|
/** @type {Readonly<ResourceLink[]>} */
|
||||||
export let resources;
|
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<PodcastLink[]>} */
|
||||||
|
export let podcasts;
|
||||||
|
|
||||||
$: use_is_current = (/** @type {string } */ href) => {
|
$: use_is_current = (/** @type {string } */ href) => {
|
||||||
let href_pathname = href;
|
let href_pathname = href;
|
||||||
|
|
||||||
@@ -57,11 +67,6 @@
|
|||||||
) => {
|
) => {
|
||||||
if (!is_current_page) return false;
|
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);
|
return $page.url.hash.includes(anchor);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -174,6 +179,37 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="text-xs font-semibold leading-6 text-mono-400">
|
||||||
|
Svelte Podcasts
|
||||||
|
</div>
|
||||||
|
<ul role="list" class="-mx-2 mt-2 space-y-1">
|
||||||
|
{#each podcasts as link}
|
||||||
|
{@const is_current = use_is_current(link.href)}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
class={clsx(
|
||||||
|
'group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6',
|
||||||
|
is_current
|
||||||
|
? 'bg-mono-50 text-primary-600'
|
||||||
|
: 'text-mono-700 hover:bg-mono-50 hover:text-primary-600',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={link.src}
|
||||||
|
height="56"
|
||||||
|
width="56"
|
||||||
|
alt=""
|
||||||
|
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border border-mono-200 bg-white text-[0.625rem] font-medium text-mono-400 group-hover:border-primary-600 group-hover:text-primary-600"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{link.label}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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<AudioSource | null>} */
|
||||||
|
export const audio_element_source = writable(null);
|
||||||
@@ -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<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),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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,116 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { announce } from '../internal';
|
|
||||||
import { user_preferences } from '../user';
|
|
||||||
import { audio_element } from './stores/audio-element';
|
|
||||||
|
|
||||||
$: $audio_element;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
return user_preferences.subscribe((prefs) => {
|
|
||||||
const el = get(audio_element);
|
|
||||||
|
|
||||||
if (!el) return announce.warn('no audio element found');
|
|
||||||
|
|
||||||
announce.info('setting preferences :: ', prefs);
|
|
||||||
el.volume = prefs.volume;
|
|
||||||
el.playbackRate = prefs.playback_rate;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- TODO: move styles to headless -->
|
|
||||||
<svelte:head>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--svpod--font: ui-sans-serif, system-ui, -apple-system,
|
|
||||||
BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
||||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
|
||||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
|
||||||
|
|
||||||
--svpod--surface--darker: rgb(0, 0, 0);
|
|
||||||
--svpod--surface--base: rgb(40, 40, 40);
|
|
||||||
--svpod--surface--lighter: rgb(60, 60, 60);
|
|
||||||
|
|
||||||
--svpod--content--darker: rgb(150, 150, 150);
|
|
||||||
--svpod--content--base: rgb(200, 200, 200);
|
|
||||||
--svpod--content--lighter: rgb(255, 255, 255);
|
|
||||||
|
|
||||||
--svpod--accent--darker: rgb(75, 15, 0);
|
|
||||||
--svpod--accent--base: rgb(180, 40, 0);
|
|
||||||
--svpod--accent--lighter: rgb(255, 55, 25);
|
|
||||||
|
|
||||||
--bg: var(--svpod--surface--darker);
|
|
||||||
--fg: var(--svpod--content--darker);
|
|
||||||
|
|
||||||
--svpod--radius--rounded: 9999px;
|
|
||||||
|
|
||||||
/* element : timeline */
|
|
||||||
|
|
||||||
--svpod--timeline-track--shape--height: 20px;
|
|
||||||
--svpod--timeline-track--shape--radius: 0;
|
|
||||||
--svpod--timeline-track--shape--border: 0;
|
|
||||||
|
|
||||||
--svpod--timeline-thumb--shape--width: 8px;
|
|
||||||
--svpod--timeline-thumb--shape--height: 32px;
|
|
||||||
--svpod--timeline-thumb--shape--radius: 6px;
|
|
||||||
--svpod--timeline-thumb--shape--border: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--reset {
|
|
||||||
text-transform: none;
|
|
||||||
font-style: normal;
|
|
||||||
text-indent: 0;
|
|
||||||
text-shadow: none;
|
|
||||||
text-align: left;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--a11y-hidden {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--button,
|
|
||||||
.svpod--icon-button {
|
|
||||||
--fg: var(--svpod--content--base);
|
|
||||||
--bg: var(--svpod--surface--base);
|
|
||||||
position: relative;
|
|
||||||
line-height: 1em;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: 0px solid transparent;
|
|
||||||
border-radius: var(--svpod--radius--rounded);
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.svpod--button:hover,
|
|
||||||
.svpod--icon-button:hover {
|
|
||||||
--fg: var(--svpod--content--lighter);
|
|
||||||
--bg: var(--svpod--surface--lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--button {
|
|
||||||
padding: 4px calc(1em);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svpod--icon-button {
|
|
||||||
padding: 8px;
|
|
||||||
min-width: 48px;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svelte:head>
|
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
* @module audio
|
* @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 AudioPlayer } from './player.svelte';
|
||||||
export { default as AudioProgress } from './progress.svelte';
|
export { default as AudioProgress } from './progress.svelte';
|
||||||
export { episode_audio, episode_details, episode_progress } from './stores';
|
|
||||||
|
|||||||
+11
-41
@@ -1,71 +1,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { AudioProgress, audio_attributes } from '.';
|
||||||
import { AudioProgress } from '.';
|
import { audio } from '../audio';
|
||||||
import { episode_audio, episode_progress } from '../audio';
|
|
||||||
import { user_preferences } from '../user';
|
import { user_preferences } from '../user';
|
||||||
import { seconds_to_timestamp } from '../utility';
|
|
||||||
|
|
||||||
/** @type {string|undefined} */
|
/** @type {string|undefined} */
|
||||||
export let src;
|
export let src;
|
||||||
|
|
||||||
/** @type {import('../audio/stores').EpisodeDetails} */
|
/** @type {import('./audio-metadata').AudioMetadata} */
|
||||||
export let metadata = {};
|
export let metadata = {};
|
||||||
|
|
||||||
let mounted = false;
|
$: src && audio.load(src, metadata || {});
|
||||||
|
|
||||||
$: is_loaded = Boolean($episode_audio?.src);
|
|
||||||
$: is_playing = $episode_audio?.is_paused === false;
|
|
||||||
$: current_time = $episode_progress?.current_time || 0;
|
|
||||||
$: duration = $episode_audio?.duration || 0;
|
|
||||||
$: timestamp_hours = Boolean(
|
|
||||||
$episode_audio?.duration && $episode_audio.duration >= 3600,
|
|
||||||
);
|
|
||||||
$: timestamp = duration && seconds_to_timestamp(duration);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!src) {
|
|
||||||
mounted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
episode_audio.load(src, metadata || {});
|
|
||||||
mounted = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
$: mounted && src && episode_audio.load(src, metadata || {});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips the audio forward by a specified number of seconds.
|
* Skips the audio forward by a specified number of seconds.
|
||||||
* @param {number} seconds - The number of seconds to skip forward.
|
* @param {number} seconds - The number of seconds to skip forward.
|
||||||
*/
|
*/
|
||||||
const skip_forward = (seconds) => episode_audio.skip(seconds, 'forward');
|
const skip_forward = (seconds) => audio.skip(seconds, 'forward');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips the audio backward by a specified number of seconds.
|
* Skips the audio backward by a specified number of seconds.
|
||||||
* @param {number} seconds - The number of seconds to skip backward.
|
* @param {number} seconds - The number of seconds to skip backward.
|
||||||
*/
|
*/
|
||||||
const skip_back = (seconds) => episode_audio.skip(seconds, 'backward');
|
const skip_back = (seconds) => audio.skip(seconds, 'backward');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to a specific time in the audio.
|
* Seeks to a specific time in the audio.
|
||||||
* @param {number} value - The time to seek to, in seconds.
|
* @param {number} value - The time to seek to, in seconds.
|
||||||
*/
|
*/
|
||||||
const seek_to = (value) => episode_audio.seek(value);
|
const seek_to = (value) => audio.seek(value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the audio playback between play and pause.
|
* Toggles the audio playback between play and pause.
|
||||||
*/
|
*/
|
||||||
const toggle = () => episode_audio.play('toggle');
|
const toggle = () => audio.play('toggle');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts playing the audio.
|
* Starts playing the audio.
|
||||||
*/
|
*/
|
||||||
const play = () => episode_audio.play('set');
|
const play = () => audio.play('set');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the audio.
|
* Pauses the audio.
|
||||||
*/
|
*/
|
||||||
const pause = () => episode_audio.pause('set');
|
const pause = () => audio.pause('set');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
@@ -80,12 +57,5 @@
|
|||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
}}
|
}}
|
||||||
episode={{
|
attributes={$audio_attributes}
|
||||||
is_loaded,
|
|
||||||
is_playing,
|
|
||||||
current_time,
|
|
||||||
duration,
|
|
||||||
timestamp_hours,
|
|
||||||
timestamp,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { episode_audio, episode_progress } from '../audio';
|
import clamp from 'just-clamp';
|
||||||
import { announce } from '../internal';
|
import { announce } from '../internal';
|
||||||
|
import { audio_attributes, audio_element } from './audio-element';
|
||||||
|
|
||||||
export let step = 10;
|
export let step = 10;
|
||||||
|
|
||||||
@@ -11,8 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
function handle_drag_start(t) {
|
function handle_drag_start(t) {
|
||||||
announce.info('drag_start :: ', t);
|
announce.info('drag_start :: ', t);
|
||||||
was_paused = $episode_audio?.is_paused || true;
|
was_paused = $audio_attributes.is_paused;
|
||||||
episode_audio.pause();
|
$audio_element?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,21 +22,33 @@
|
|||||||
function handle_drag_end(t) {
|
function handle_drag_end(t) {
|
||||||
announce.info('drag_end :: ', t);
|
announce.info('drag_end :: ', t);
|
||||||
if (was_paused) return;
|
if (was_paused) return;
|
||||||
else episode_audio.play();
|
else $audio_element?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to time audio
|
||||||
|
* @param {number} seconds - Seconds to seek
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const handle_seek_to = (seconds) => {
|
||||||
|
const el = $audio_element;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
el.currentTime = clamp(0, el.duration, seconds);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $episode_audio && $episode_progress}
|
{#if $audio_attributes.is_loaded}
|
||||||
<input
|
<input
|
||||||
class={$$restProps.class}
|
class={$$restProps.class}
|
||||||
style="display:block; width:100%;"
|
style="display:block; width:100%;"
|
||||||
type="range"
|
type="range"
|
||||||
data-paused={$episode_audio.is_paused ? 'true' : 'false'}
|
data-paused={$audio_attributes.is_paused ? 'true' : 'false'}
|
||||||
min={0}
|
min={0}
|
||||||
{step}
|
{step}
|
||||||
max={$episode_audio.duration || step}
|
max={$audio_attributes.duration}
|
||||||
value={$episode_progress.current_time}
|
value={$audio_attributes.current_time}
|
||||||
on:change={(e) => episode_audio.seek(e.currentTarget.valueAsNumber)}
|
on:change={(e) => handle_seek_to(e.currentTarget.valueAsNumber)}
|
||||||
on:touchstart={() => handle_drag_start('touchstart')}
|
on:touchstart={() => handle_drag_start('touchstart')}
|
||||||
on:mousedown={() => handle_drag_start('mousedown')}
|
on:mousedown={() => handle_drag_start('mousedown')}
|
||||||
on:touchend={() => handle_drag_end('touchend')}
|
on:touchend={() => handle_drag_end('touchend')}
|
||||||
|
|||||||
@@ -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<HTMLAudioElement | undefined>}
|
|
||||||
* @exports audio_element
|
|
||||||
*/
|
|
||||||
export const audio_element = readable(undefined, (set) => {
|
|
||||||
if (!BROWSER) return;
|
|
||||||
const ID = 'svpod--generated-audio-element';
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const existing_element = document.getElementById(ID);
|
|
||||||
|
|
||||||
/** @type {HTMLAudioElement} */
|
|
||||||
const el =
|
|
||||||
existing_element instanceof HTMLAudioElement
|
|
||||||
? existing_element
|
|
||||||
: document.createElement('audio');
|
|
||||||
|
|
||||||
el.id = ID;
|
|
||||||
el.setAttribute('preload', 'metadata');
|
|
||||||
el.muted = false;
|
|
||||||
el.autoplay = false;
|
|
||||||
el.controls = false;
|
|
||||||
|
|
||||||
/** @type {UserPreferences} */
|
|
||||||
const preferences = get(user_preferences);
|
|
||||||
el.playbackRate = preferences.playback_rate;
|
|
||||||
el.volume = preferences.volume;
|
|
||||||
|
|
||||||
if (!existing_element) document.body.appendChild(el);
|
|
||||||
|
|
||||||
/*
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -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<EpisodeState, 'src'>} */
|
|
||||||
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<EpisodeState | null>}
|
|
||||||
*/
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object.<string, unknown>} EpisodeDetails
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Episode details store.
|
|
||||||
* @type {import('svelte/store').Writable<EpisodeDetails | null>}
|
|
||||||
* @description This store holds the metadata for a single episode.
|
|
||||||
*/
|
|
||||||
export const episode_details = writable(null);
|
|
||||||
@@ -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<EpisodeProgress>}
|
|
||||||
*/
|
|
||||||
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());
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './audio-element';
|
|
||||||
export * from './episode-data';
|
|
||||||
export * from './episode-details';
|
|
||||||
export * from './episode-progress';
|
|
||||||
+6
-3
@@ -1,4 +1,7 @@
|
|||||||
export * from './audio';
|
import { default as AudioPlayer } from './audio/player.svelte';
|
||||||
export { AudioPlayer as default } from './audio';
|
|
||||||
|
export * from './audio/actions';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './utility';
|
|
||||||
|
export { AudioPlayer };
|
||||||
|
export default AudioPlayer;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { persisted } from 'svelte-local-storage-store';
|
import { persisted } from 'svelte-local-storage-store';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { episode_audio, episode_progress } from '../audio';
|
import { audio_element } from '../audio';
|
||||||
import { use_url, announce } from '../internal';
|
import { announce, use_url } from '../internal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User progress object type.
|
* User progress object type.
|
||||||
@@ -14,7 +14,7 @@ import { use_url, announce } from '../internal';
|
|||||||
* @type {UserProgress}
|
* @type {UserProgress}
|
||||||
*/
|
*/
|
||||||
const DEFAULT_USER_PROGRESS = {};
|
const DEFAULT_USER_PROGRESS = {};
|
||||||
import {} from 'svelte-local-storage-store';
|
|
||||||
/**
|
/**
|
||||||
* User progress store
|
* User progress store
|
||||||
* @type {import('svelte/store').Writable<UserProgress>}
|
* @type {import('svelte/store').Writable<UserProgress>}
|
||||||
@@ -27,13 +27,13 @@ const USER_PROGRESS_STORE = persisted('USER_PROGRESS', DEFAULT_USER_PROGRESS);
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const save_user_progress = () => {
|
const save_user_progress = () => {
|
||||||
const audio = get(episode_audio);
|
const audio = get(audio_element);
|
||||||
if (!audio?.src) return;
|
if (!audio?.src) return;
|
||||||
|
|
||||||
announce.info('saving progress: ', audio.src);
|
announce.info('saving progress: ', audio.src);
|
||||||
|
|
||||||
const pathname = use_url(audio.src).pathname;
|
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) => ({
|
USER_PROGRESS_STORE.update((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[pathname]: current_time,
|
[pathname]: current_time,
|
||||||
|
|||||||
+58
-13
@@ -3,7 +3,6 @@
|
|||||||
import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte';
|
import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte';
|
||||||
import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte';
|
import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte';
|
||||||
import github from 'svelte-highlight/styles/github';
|
import github from 'svelte-highlight/styles/github';
|
||||||
import { AudioContext } from 'svelte-podcast';
|
|
||||||
import { circIn, circOut } from 'svelte/easing';
|
import { circIn, circOut } from 'svelte/easing';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import '../app.postcss';
|
import '../app.postcss';
|
||||||
@@ -18,21 +17,54 @@
|
|||||||
label: 'Setup',
|
label: 'Setup',
|
||||||
href: '/setup',
|
href: '/setup',
|
||||||
sections: [
|
sections: [
|
||||||
{ label: 'installation', anchor: 'installation' },
|
{ label: 'Installation', anchor: 'installation' },
|
||||||
{ label: 'Loader', anchor: 'add_loader' },
|
{ label: 'Audio Sources', anchor: 'audio_sources' },
|
||||||
|
{ label: 'Type Safety', anchor: 'type_safety' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'API',
|
label: 'API',
|
||||||
href: '/api',
|
href: '/api',
|
||||||
sections: [
|
sections: [
|
||||||
{ label: 'Episode Audio', anchor: 'episode_audio' },
|
{ label: 'audio', anchor: 'audio' },
|
||||||
{ label: 'Load Audio Source', anchor: 'load_audio_source' },
|
{ label: 'audio_data', anchor: 'audio_data' },
|
||||||
{ label: 'Episode Details', anchor: 'episode_details' },
|
{ label: 'audio.subscribe', anchor: 'audio.subscribe' },
|
||||||
{ label: 'Episode Progress', anchor: 'episode_progress' },
|
{ label: 'audio.load', anchor: 'audio.load' },
|
||||||
{ label: 'Seconds To Timestamp', anchor: 'seconds_to_timestamp' },
|
{ label: 'audio.unload', anchor: 'audio.unload' },
|
||||||
{ label: 'User Preferences', anchor: 'user_preferences' },
|
{ label: 'audio.play', anchor: 'audio.play' },
|
||||||
{ label: 'User Progress', anchor: 'user_progress' },
|
{ 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;
|
let is_menu_open = false;
|
||||||
|
|
||||||
const open_menu = () => (is_menu_open = true);
|
const open_menu = () => (is_menu_open = true);
|
||||||
@@ -65,8 +110,6 @@
|
|||||||
{@html github}
|
{@html github}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<AudioContext />
|
|
||||||
|
|
||||||
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
|
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none relative z-50 lg:hidden"
|
class="pointer-events-none relative z-50 lg:hidden"
|
||||||
@@ -122,6 +165,7 @@
|
|||||||
class="pb-2"
|
class="pb-2"
|
||||||
pages={page_links}
|
pages={page_links}
|
||||||
resources={resource_links}
|
resources={resource_links}
|
||||||
|
podcasts={podcast_links}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -134,6 +178,7 @@
|
|||||||
class="border-r border-mono-200"
|
class="border-r border-mono-200"
|
||||||
pages={page_links}
|
pages={page_links}
|
||||||
resources={resource_links}
|
resources={resource_links}
|
||||||
|
podcasts={podcast_links}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,7 +224,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="py-10 lg:pl-72">
|
<main class="py-10 lg:pl-72">
|
||||||
<div class="px-4 sm:px-6 lg:px-8">
|
<div class="px-2">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+238
-276
@@ -1,242 +1,289 @@
|
|||||||
<script>
|
<script>
|
||||||
import { DocsPage } from '$src/layout/page';
|
import { DocsPage } from '$src/layout/page';
|
||||||
import { Highlight } from 'svelte-highlight';
|
import { Highlight, HighlightSvelte } from 'svelte-highlight';
|
||||||
import lang_ts from 'svelte-highlight/languages/typescript';
|
import lang_ts from 'svelte-highlight/languages/typescript';
|
||||||
import {
|
import {
|
||||||
load_audio_local,
|
audio_load,
|
||||||
load_audio_remote,
|
audio_mute,
|
||||||
override_episode_state,
|
audio_pause,
|
||||||
|
audio_play,
|
||||||
|
audio_seek,
|
||||||
|
audio_skip,
|
||||||
|
audio_subscribe,
|
||||||
|
audio_unload,
|
||||||
|
audio_unmute,
|
||||||
seconds_to_timestamp,
|
seconds_to_timestamp,
|
||||||
|
user_preferences_clear,
|
||||||
|
user_preferences_set_playback_rate,
|
||||||
|
user_preferences_set_volume,
|
||||||
|
user_preferences_subscribe,
|
||||||
|
user_progress_clear,
|
||||||
|
user_progress_get,
|
||||||
|
user_progress_save,
|
||||||
|
user_progress_subscribe,
|
||||||
} from './code';
|
} from './code';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DocsPage
|
<DocsPage title="API" let:Section let:SectionArticle let:TableSchema>
|
||||||
title="API"
|
<Section title="audio">
|
||||||
let:Section
|
<SectionArticle title="audio data">
|
||||||
let:SectionArticle
|
|
||||||
let:TableModule
|
|
||||||
let:TableSchema
|
|
||||||
>
|
|
||||||
<Section title="episode_audio">
|
|
||||||
<!-- methods -->
|
|
||||||
<h3>Methods</h3>
|
|
||||||
|
|
||||||
<TableModule
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
method: '.subscribe()',
|
|
||||||
description: 'You can subscribe to changes in the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.load(\n src:string, \n details:EpisodeDetails\n)',
|
|
||||||
description:
|
|
||||||
'Load a new audio source. This will stop the current audio source and replace it with the new one.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.unload()',
|
|
||||||
description:
|
|
||||||
'Unload the current audio source. This will stop the current audio source and remove it from the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.play(\n action: "set" | "toggle" = "set"\n)',
|
|
||||||
description: 'Set or toggle the play state',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.pause(\n action: "set" | "toggle" = "set"\n)',
|
|
||||||
description: 'Set or toggle the pause state',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.mute(\n action: "set" | "toggle" = "set"\n)',
|
|
||||||
description: 'Set or toggle the mute state',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.unmute(\n action: "set" | "toggle" = "set"\n)',
|
|
||||||
description: 'Set or toggle the unmute state',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method:
|
|
||||||
'.seek(\n seconds: number, \n from: "from-start" | "from-end" = "from-start"\n)',
|
|
||||||
description:
|
|
||||||
'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.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method:
|
|
||||||
'.skip(\n seconds: number, \n type: "forward" | "backward" = "forward"\n)',
|
|
||||||
description:
|
|
||||||
'Skip forward or backward in the audio by a specific number of seconds.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- data -->
|
|
||||||
<h3>EpisodeState</h3>
|
|
||||||
|
|
||||||
<TableSchema
|
<TableSchema
|
||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
property: 'will_autoplay',
|
property: 'is_loaded',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether episodes will autoplay once loaded.',
|
description: 'Whether the audio element is loaded or not',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'is_paused',
|
property: 'is_paused',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the episode is paused.',
|
description: 'Whether the audio element is paused or not',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'current_time',
|
||||||
|
type: 'number',
|
||||||
|
description:
|
||||||
|
'The current time of the audio element in seconds',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'The duration of the episode in seconds.',
|
description: 'The duration of the audio element in seconds',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'src',
|
property: 'timestamp_current',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The source of the episode.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: 'start_at',
|
|
||||||
type: 'number',
|
|
||||||
description: 'The starting point of the episode in seconds.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: 'details',
|
|
||||||
type: 'EpisodeDetails | null',
|
|
||||||
description:
|
description:
|
||||||
'The details of the episode or null if there are none.',
|
'The current time of the audio element in timestamp format (hh:mm:ss)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'timestamp_end',
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The end time of the audio element in timestamp format (hh:mm:ss)',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
<SectionArticle title="Load audio source">
|
<!--
|
||||||
|
audio.subscribe
|
||||||
|
audio.load
|
||||||
|
audio.unload
|
||||||
|
audio.play
|
||||||
|
audio.pause
|
||||||
|
audio.mute
|
||||||
|
audio.unmute
|
||||||
|
audio.seek
|
||||||
|
audio.skip
|
||||||
|
-->
|
||||||
|
<SectionArticle title="audio.subscribe">
|
||||||
<p>
|
<p>
|
||||||
All you need to load an episode is a URL to an audio file.
|
Subscribe to specific changes in the audio attributes. This will
|
||||||
svelte-podcast uses a html audio element under the hood, so any
|
return a readable store that you can use to access the attributes.
|
||||||
audio file compatible with the autio element is also compatible with
|
|
||||||
this package.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
<h4>Using a remote file (URL)</h4>
|
<HighlightSvelte code={audio_subscribe} />
|
||||||
|
|
||||||
<p>
|
|
||||||
An <em>audio url</em> could be a URL to an MP3 file from an RSS
|
|
||||||
feed, like this:
|
|
||||||
<code>https://media.transistor.fm/27a058c9/27b595e2.mp3</code>. It
|
|
||||||
could also be a path to a static file on your server.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="codeblock not-prose flex flex-col items-stretch gap-2 py-1"
|
|
||||||
>
|
|
||||||
<Highlight code={load_audio_remote} language={lang_ts} />
|
|
||||||
</div>
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
<h4>Using a local file (relative path)</h4>
|
<SectionArticle title="audio.load">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you're using SvelteKit, you can store <em>static files</em> in
|
Load a new audio source. This will stop the current audio source and
|
||||||
the /static directory. When your site is built, everything in the
|
replace it with the new one.
|
||||||
static directory will be at the root of your site. If you have a
|
|
||||||
file in <code>/static/episides/episode-01.mp3</code> you could load
|
|
||||||
it as <code>/episides/episode-01.mp3</code>
|
|
||||||
</p>
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_load} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
<div
|
<SectionArticle title="audio.unload">
|
||||||
class="codeblock not-prose flex flex-col items-stretch gap-2 py-1"
|
<p>
|
||||||
>
|
Unload the current audio source. This will stop the current audio
|
||||||
<Highlight code={load_audio_local} language={lang_ts} />
|
source and remove it from the audio state.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_unload} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="audio.play">
|
||||||
|
<p>Set or toggle the play state.</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_play} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<p>Set or toggle the mute state.</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_mute} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_seek} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="audio.skip">
|
||||||
|
<p>
|
||||||
|
Skip forward or backward in the audio by a specific number of
|
||||||
|
seconds.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={audio_skip} language={lang_ts} />
|
||||||
</div>
|
</div>
|
||||||
</SectionArticle>
|
</SectionArticle>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="episode_details">
|
<Section title="user_preferences">
|
||||||
<h3>Methods</h3>
|
<SectionArticle title="user_preferences data">
|
||||||
|
|
||||||
<TableModule
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
method: '.subscribe() | $episode_details',
|
|
||||||
description: 'You can subscribe to changes in the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.set(\n data:EpisodeDetails\n)',
|
|
||||||
description: 'You can set the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.update((\n data:EpisodeDetails) => EpisodeDetails\n)',
|
|
||||||
description:
|
|
||||||
'You can update the audio state. This is useful if you want to update the state based on the previous state.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- data -->
|
|
||||||
<h3>EpisodeState</h3>
|
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={`Record<string, unknown>`} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Episode state holds the metadata for the current episode. You derermine
|
|
||||||
the shape of the this data.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
For complete type safety, we recommend defining this type yourself in a
|
|
||||||
.d.ts file. In SvelteKit, you should do this in <code>
|
|
||||||
/src/app.d.ts
|
|
||||||
</code>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h4>Override EpisodeState</h4>
|
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={override_episode_state} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<!-- episode_progress -->
|
|
||||||
<Section title="episode_progress">
|
|
||||||
<!-- methods -->
|
|
||||||
<h3>Methods</h3>
|
|
||||||
<TableModule
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
method: '.subscribe() | $episode_progress',
|
|
||||||
description: 'You can subscribe to changes in the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.set(\n data:EpisodeProgress\n)',
|
|
||||||
description: 'You can set the audio state.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method:
|
|
||||||
'.update(\n (data:EpisodeProgress) => EpisodeProgress\n)',
|
|
||||||
description:
|
|
||||||
'You can update the audio state. This is useful if you want to update the state based on the previous state.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- data -->
|
|
||||||
<h3>EpisodeProgress</h3>
|
|
||||||
<TableSchema
|
<TableSchema
|
||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
property: 'current_time',
|
property: 'playback_rate',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'The current time of the episode in seconds.',
|
description: 'The audio player playback rate (speed)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
property: 'timestamp',
|
property: 'volume',
|
||||||
type: 'string',
|
type: 'number',
|
||||||
description:
|
description: 'The audio player volume.',
|
||||||
"The current time of the episode in the format 'mm:ss'.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: 'has_ended',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether the episode has ended or not.',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
user_preferences.subscribe
|
||||||
|
user_preferences.set_playback_rate
|
||||||
|
user_preferences.set_volume
|
||||||
|
user_preferences.clear
|
||||||
|
-->
|
||||||
|
<SectionArticle title="user_preferences.subscribe">
|
||||||
|
<p>
|
||||||
|
Subscribe to changes in the user preferences. This will return a
|
||||||
|
readable store that you can use to access the user preferences.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<HighlightSvelte code={user_preferences_subscribe} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_preferences.set_playback_rate">
|
||||||
|
<p>
|
||||||
|
Set the playback rate. This will update the user preferences and
|
||||||
|
apply the new playback rate to the audio.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight
|
||||||
|
code={user_preferences_set_playback_rate}
|
||||||
|
language={lang_ts}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_preferences.set_volume">
|
||||||
|
<p>
|
||||||
|
Set the volume. This will update the user preferences and apply the
|
||||||
|
new volume to the audio.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={user_preferences_set_volume} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_preferences.clear">
|
||||||
|
<p>
|
||||||
|
Clear the user preferences. This will reset the user preferences to
|
||||||
|
the default values.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={user_preferences_clear} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<!-- user_progress -->
|
||||||
|
<Section title="user_progress">
|
||||||
|
<SectionArticle title="user_progress data">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={`Record<string, number>`} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
* user_progress.subscribe - Subscribes to user progress store
|
||||||
|
* user_progress.get - Gets user progress for a given audio source
|
||||||
|
* user_progress.clear - Clears user progress store
|
||||||
|
* user_progress.save - Saves user progress
|
||||||
|
-->
|
||||||
|
<SectionArticle title="user_progress.subscribe">
|
||||||
|
<p>
|
||||||
|
Subscribe to changes in the user progress. This will return a
|
||||||
|
readable store that you can use to access the user progress.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<HighlightSvelte code={user_progress_subscribe} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_progress.get">
|
||||||
|
<p>
|
||||||
|
Get the user progress for a given audio source. This will return the
|
||||||
|
timestamp in seconds.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={user_progress_get} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_progress.clear">
|
||||||
|
<p>
|
||||||
|
Clear the user progress. This will reset the user progress to the
|
||||||
|
default values.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={user_progress_clear} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
|
|
||||||
|
<SectionArticle title="user_progress.save">
|
||||||
|
<p>
|
||||||
|
Save the user progress for a given audio source. This will update
|
||||||
|
the user progress with the new timestamp.
|
||||||
|
</p>
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={user_progress_save} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
</SectionArticle>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<!-- seconds_to_timestamp -->
|
<!-- seconds_to_timestamp -->
|
||||||
@@ -261,89 +308,4 @@
|
|||||||
<Highlight code={seconds_to_timestamp} language={lang_ts} />
|
<Highlight code={seconds_to_timestamp} language={lang_ts} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<!-- user_preferences -->
|
|
||||||
<Section title="user_preferences">
|
|
||||||
<!-- methods -->
|
|
||||||
<h3>Methods</h3>
|
|
||||||
<TableModule
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
method: '.subscribe() | $episode_audio',
|
|
||||||
description:
|
|
||||||
'You can subscribe to changes in the users preferences.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.set_playback_rate(\n value:number\n)',
|
|
||||||
description: 'You can set the users playback rate.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.set_volume(\n value:number\n)',
|
|
||||||
description: 'You can set the users volume.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.clear()',
|
|
||||||
description: 'You can clear the users preferences.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- data -->
|
|
||||||
<h3>UserPreferences</h3>
|
|
||||||
<TableSchema
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
property: 'playback_rate',
|
|
||||||
type: 'number',
|
|
||||||
description: 'The audio player playback rate (speed)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: 'volume',
|
|
||||||
type: 'number',
|
|
||||||
description: 'The audio player volume.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<!-- user_progress -->
|
|
||||||
<Section title="user_progress">
|
|
||||||
<!-- methods -->
|
|
||||||
<h3>Methods</h3>
|
|
||||||
<TableModule
|
|
||||||
rows={[
|
|
||||||
{
|
|
||||||
method: '.subscribe() | $episode_progress',
|
|
||||||
description:
|
|
||||||
'You can subscribe to changes in the users progress.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.get(\n src:string\n): number | undefined',
|
|
||||||
description:
|
|
||||||
'You can get the users progress for a given audio source.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.save(\n src:string, \n value:number\n)',
|
|
||||||
description:
|
|
||||||
'You can save the users progress for a given audio source.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: '.clear()',
|
|
||||||
description: 'You can clear the users progress.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- data -->
|
|
||||||
<h3>UserProgress</h3>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="not-prose codeblock">
|
|
||||||
<Highlight code={`Record<string, number>`} language={lang_ts} />
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</DocsPage>
|
</DocsPage>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
@@ -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');
|
||||||
@@ -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');
|
||||||
@@ -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');
|
||||||
@@ -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');
|
||||||
@@ -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');
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// return the current user preferences
|
||||||
|
const attributes = $audio;
|
||||||
|
|
||||||
|
// or subscribe to changes in the user preferences
|
||||||
|
audio.subscribe((attr) => {
|
||||||
|
// do something with the new preferences whenever they change
|
||||||
|
console.log(attr);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Current timestamp {attributes.timestamp_current}.</p>
|
||||||
|
|
||||||
|
<p>{attributes.is_paused ? 'Paused...' : 'Playing!'}.</p>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// unload the current audio source
|
||||||
|
audio.unload();
|
||||||
@@ -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');
|
||||||
@@ -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_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 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';
|
import { default as seconds_to_timestamp_src } from './seconds-to-timestamp?raw';
|
||||||
|
import { default as user_preferences_clear_src } from './user_preferences-clear?raw';
|
||||||
const format_code = (/** @type {string} */ code) => code.replaceAll('\t', ' ');
|
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_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_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 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);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { episode_audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- load the episode on click -->
|
<!-- load the episode on click -->
|
||||||
<button
|
<button
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
episode_audio.load('/episode-audio.mp3', {
|
audio.load('/episode-audio.mp3', {
|
||||||
/* optional metadata */
|
/* optional metadata */
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- unload the episode on click -->
|
<!-- unload the episode on click -->
|
||||||
<button on:click={() => episode_audio.unload()}>Unload Episode</button>
|
<button on:click={() => audio.unload()}>Unload Episode</button>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { episode_audio } from 'svelte-podcast';
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// load the episode on mount without any metadata
|
// load the episode on mount without any metadata
|
||||||
episode_audio.load('/episode-audio.mp3', {
|
audio.load('/episode-audio.mp3', {
|
||||||
/* optional metadata */
|
/* optional metadata */
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { episode_audio } from 'svelte-podcast';
|
|
||||||
|
|
||||||
episode_audio.load(
|
|
||||||
// path to file
|
|
||||||
'/episode-audio.mp3',
|
|
||||||
|
|
||||||
// custom metadata
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { episode_audio } from 'svelte-podcast';
|
|
||||||
|
|
||||||
episode_audio.load(
|
|
||||||
// url
|
|
||||||
'https://media.transistor.fm/27a058c9/27b595e2.mp3',
|
|
||||||
|
|
||||||
// custom metadata
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { seconds_to_timestamp } from 'svelte-podcast';
|
import { seconds_to_timestamp } from 'svelte-podcast/utility';
|
||||||
|
|
||||||
// Example
|
// Example
|
||||||
seconds_to_timestamp(5173); // 01:26:13
|
seconds_to_timestamp(5173); // 01:26:13
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { user_preferences } from '../../../lib/user';
|
||||||
|
|
||||||
|
// Clears all user preferences
|
||||||
|
user_preferences.clear();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { user_preferences } from '../../../lib/user';
|
||||||
|
|
||||||
|
// Sets the playback speed to 150%
|
||||||
|
user_preferences.set_playback_rate(1.5);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { user_preferences } from '../../../lib/user';
|
||||||
|
|
||||||
|
// Sets the volume to 75%
|
||||||
|
user_preferences.set_volume(0.75);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
import { user_preferences } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// return the current user preferences
|
||||||
|
const preferences = $user_preferences;
|
||||||
|
|
||||||
|
// or subscribe to changes in the user preferences
|
||||||
|
user_preferences.subscribe((prefs) => {
|
||||||
|
// do something with the new preferences whenever they change
|
||||||
|
console.log(prefs);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Listening at {preferences.playback_rate * 100}% speed.</p>
|
||||||
|
|
||||||
|
<p>Volume set to {preferences.volume * 100}%.</p>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { user_progress } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// Clear the user's progress for ALL episodes.
|
||||||
|
user_progress.clear();
|
||||||
@@ -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');
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { user_progress } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// Save the user's progress for the current episode.
|
||||||
|
user_progress.save();
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
import { user_progress } from 'svelte-podcast';
|
||||||
|
|
||||||
|
// return the current user progress
|
||||||
|
const progress = $user_progress;
|
||||||
|
|
||||||
|
// or subscribe to changes in the user progress
|
||||||
|
user_progress.subscribe((prog) => {
|
||||||
|
// do something with the new progress whenever they change
|
||||||
|
console.log(prog);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Listened to {progress['https://example.com/episodes/1']} seconds of episode 1.
|
||||||
|
</p>
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import PlayerStack from '$src/components/example-player/player-stack.svelte';
|
|
||||||
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
|
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
|
||||||
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 PreviewComponent from '$src/layout/preview-component.svelte';
|
import PreviewComponent from '$src/layout/preview-component.svelte';
|
||||||
import { episode_audio } from 'svelte-podcast';
|
|
||||||
|
|
||||||
$: console.log('details :: ', $episode_audio?.details);
|
|
||||||
|
|
||||||
$: player_widget = {
|
$: player_widget = {
|
||||||
current_time: true,
|
current_time: true,
|
||||||
@@ -16,24 +12,12 @@
|
|||||||
skip_forward: 30,
|
skip_forward: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
$: player_stack = {
|
|
||||||
playback_rate: true,
|
|
||||||
timestamps: true,
|
|
||||||
skip_back: 10,
|
|
||||||
skip_forward: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type { string | undefined} */
|
/** @type { string | undefined} */
|
||||||
let audio_src = episodes.knomii.src;
|
let audio_src = episodes.knomii.src;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DocsPage
|
<DocsPage title="Examples" let:Section>
|
||||||
title="Examples"
|
<p class="text-xl font-semibold text-red-500">Work in progress!</p>
|
||||||
let:Section
|
|
||||||
let:SectionArticle
|
|
||||||
let:TableModule
|
|
||||||
let:TableSchema
|
|
||||||
>
|
|
||||||
<Section title="PlayerWidget">
|
<Section title="PlayerWidget">
|
||||||
<PreviewComponent name="PlayerWidget">
|
<PreviewComponent name="PlayerWidget">
|
||||||
<svelte:fragment slot="options">
|
<svelte:fragment slot="options">
|
||||||
@@ -82,45 +66,4 @@
|
|||||||
<PlayerWidget src={audio_src} include={player_widget} />
|
<PlayerWidget src={audio_src} include={player_widget} />
|
||||||
</PreviewComponent>
|
</PreviewComponent>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="PlayerStack">
|
|
||||||
<PreviewComponent name="PlayerStack">
|
|
||||||
<svelte:fragment slot="options">
|
|
||||||
<div>
|
|
||||||
<label for="pw_playback_rate">playback_rate</label>
|
|
||||||
<input
|
|
||||||
id="pw_playback_rate"
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={player_stack.playback_rate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="pw_timestamps">timestamps</label>
|
|
||||||
<input
|
|
||||||
id="pw_timestamps"
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={player_stack.timestamps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="pw_skip_back">skip_back</label>
|
|
||||||
<input
|
|
||||||
id="pw_skip_back"
|
|
||||||
type="number"
|
|
||||||
bind:value={player_stack.skip_back}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="pw_skip_forward">skip_forward</label>
|
|
||||||
<input
|
|
||||||
id="pw_skip_forward"
|
|
||||||
type="number"
|
|
||||||
bind:value={player_stack.skip_forward}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<PlayerStack src={audio_src} class="max-w-sm" include={player_stack} />
|
|
||||||
</PreviewComponent>
|
|
||||||
</Section>
|
|
||||||
</DocsPage>
|
</DocsPage>
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { DocsPage } from '$src/layout/page';
|
import { DocsPage } from '$src/layout/page';
|
||||||
import { add_audio_loader, install } from './code';
|
import { Highlight } from 'svelte-highlight';
|
||||||
|
|
||||||
import { Highlight, HighlightSvelte } from 'svelte-highlight';
|
|
||||||
import lang_shell from 'svelte-highlight/languages/shell';
|
import lang_shell from 'svelte-highlight/languages/shell';
|
||||||
|
import lang_ts from 'svelte-highlight/languages/typescript';
|
||||||
|
import {
|
||||||
|
install,
|
||||||
|
load_audio_advanced,
|
||||||
|
load_audio_local,
|
||||||
|
load_audio_remote,
|
||||||
|
override_episode_state,
|
||||||
|
} from './code';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DocsPage title="Setup" let:Section>
|
<DocsPage title="Setup" let:Section>
|
||||||
@@ -17,37 +23,82 @@
|
|||||||
<Highlight language={lang_shell} code={install} />
|
<Highlight language={lang_shell} code={install} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section title="Add Loader">
|
|
||||||
<p class="text-primary-600">
|
<Section title="Audio Sources">
|
||||||
<b class="font-medium">Important</b>: The AudioLoader component does
|
<p>
|
||||||
<em>not</em>
|
To get started, you'll need an audio file. velte-podcast uses a html <code
|
||||||
render any UI. However it <em>is required</em> to use svelte-podcast.
|
>{'<audio />'}</code
|
||||||
|
> element under the hood, so any file compatible with the audio element
|
||||||
|
will work.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This component is responsible for loading the audio sources and
|
You can provide your audio files via absolute URL's - hosted online, or
|
||||||
persisting the audio state between page loads. This means that your
|
relative paths - hosted within your project.
|
||||||
audio will continue playing even if the user refreshes the page, or you
|
|
||||||
use different UI for the media player.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3>Using a remote file (URL)</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Add the AudioLoader component to your app. It should be as close to the
|
An <em>audio url</em> could be a URL to an MP3 file from an RSS feed,
|
||||||
root of your app as possible to ensure the audio state behaves as
|
like this:
|
||||||
expected in nested routes.
|
<code>https://media.transistor.fm/27a058c9/27b595e2.mp3</code>. It
|
||||||
|
could also be a path to a static file on your server.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>You must have one of these for svelte-podcast to work.</li>
|
|
||||||
<li>You should also only load one instance of this at a time</li>
|
|
||||||
<li>
|
|
||||||
We recommend you loading it at the base of your app in your
|
|
||||||
layout.svelte file.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
|
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
|
||||||
<HighlightSvelte code={add_audio_loader} />
|
<Highlight code={load_audio_remote} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Using a local file (relative path)</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you're using SvelteKit, you can store <em>static files</em> 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
|
||||||
|
<code>/static/episides/episode-01.mp3</code>
|
||||||
|
you could load it as <code>/episides/episode-01.mp3</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
|
||||||
|
<Highlight code={load_audio_local} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Additional options</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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 <a
|
||||||
|
href="/api/#audio.load">api docs</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
|
||||||
|
<Highlight code={load_audio_advanced} language={lang_ts} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Type Safety">
|
||||||
|
<p>
|
||||||
|
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 <code>.d.ts</code> file.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In this example, we'll assume that your metadata consists of an episode
|
||||||
|
title, episode artwork, and an optional episode guest name.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="not-prose codeblock">
|
||||||
|
<Highlight code={override_episode_state} language={lang_ts} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you're using SvelteKit, we recommend keeping this in <code
|
||||||
|
>src/app.d.ts</code
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
</DocsPage>
|
</DocsPage>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { AudioContext } from 'svelte-podcast';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AudioContext />
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { format_code } from '$src/layout/helper';
|
import { format_code } from '$src/layout/helper';
|
||||||
import { default as add_audio_loader_src } from './add-audio-loader.svelte?raw';
|
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 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 add_audio_loader = format_code(add_audio_loader_src);
|
||||||
export const install = format_code(install_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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
|
audio.load('/episode-audio.mp3');
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { audio } from 'svelte-podcast';
|
||||||
|
|
||||||
|
audio.load('https://media.transistor.fm/27a058c9/27b595e2.mp3');
|
||||||
+2
-2
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
declare module 'svelte-podcast' {
|
declare module 'svelte-podcast' {
|
||||||
interface EpisodeDetails {
|
interface EpisodeDetails {
|
||||||
// define your own properties here
|
|
||||||
title: string;
|
title: string;
|
||||||
artwork?: string;
|
artwork: string;
|
||||||
|
guest_name: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+39
-7
@@ -3,16 +3,48 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": 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,
|
"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
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user