This commit is contained in:
Ollie Taylor
2023-07-28 22:27:47 +01:00
committed by GitHub
parent dde9bec49c
commit bba24215eb
25 changed files with 735 additions and 737 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'svelte-podcast': minor
---
Improve example components
+5
View File
@@ -0,0 +1,5 @@
---
'svelte-podcast': minor
---
improve progress component style overrides
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "svelte-podcast",
"version": "0.8.0",
"license": "MIT",
"author": "Ollie Taylor",
"author": "Ollie Taylor (https://olliejt.com)",
"homepage": "https://github.com/OllieJT/svelte-podcast/blob/main/README.md",
"bugs": {
"url": "https://github.com/OllieJT/svelte-podcast/issues"
@@ -1,444 +0,0 @@
<script>
import { Pause, Play } from '@inqling/svelte-icons/heroicon-20-solid';
import { AudioPlayer, user_preferences } from 'svelte-podcast';
import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
/** @type {string | undefined} */
export let src;
/** @type {import('svelte-podcast/audio').AudioMetadata} */
export let metadata = {};
export let hide_duration = false;
export let hide_current_time = false;
export let hide_playback_rate = false;
export let hide_skip_back = false;
export let hide_skip_forward = 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>
<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
{...$$restProps}
class="svpod--container svpod--reset"
data-loaded={attributes.is_loaded ? 'true' : 'false'}
>
{#if !hide_skip_back}
<button
on:click={() => action.skip_back(skip_back)}
class="svpod--reset svpod--toggle-pause"
type="button"
>
<Skip value={skip_back} type="backward" />
</button>
{/if}
<!-- toggle play -->
{#if attributes.is_loaded}
<button
on:click={() => action.toggle()}
class="svpod--reset svpod--toggle-pause"
type="button"
>
<A11yIcon
icon={attributes.is_paused ? Play : Pause}
label={attributes.is_paused ? 'Play' : 'Pause'}
/>
</button>
{:else}
<div class="svpod--reset svpod--toggle-pause">
<A11yIcon icon={Spinner} label="Waiting for audio..." />
</div>
{/if}
{#if !hide_skip_forward}
<button
on:click={() => action.skip_forward(skip_forward)}
class="svpod--reset svpod--toggle-pause"
type="button"
>
<Skip value={skip_forward} type="forward" />
</button>
{/if}
{#if !hide_current_time}
<div class="svpod--timestamp">
<Timestamp
value={attributes.current_time}
force_hours={attributes.duration >= 3600}
/>
</div>
{/if}
<div class="svpod--timeline">
<Player.AudioProgress />
</div>
{#if !hide_duration}
<div class="svpod--timestamp">
<Timestamp value={attributes.duration} />
</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);
action.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>
</AudioPlayer>
<style lang="postcss">
.svpod--container {
--fg: var(--svpod--content--base);
--bg: var(--svpod--surface--darker);
--padding: 2px;
--radius: 0.5em;
--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: row;
align-items: stretch;
font-size: 16px;
line-height: 1em;
min-width: 320px;
max-width: 100%;
border-radius: var(--radius);
font-weight: 600;
padding: var(--padding);
gap: var(--padding);
}
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);
}
.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>
@@ -1,10 +0,0 @@
<script>
/** @type {import('@inqling/svelte-icons').SvelteIcon} */
export let icon;
/** @type {string} */
export let label;
</script>
<svelte:component this={icon} />
<span class="svpod--a11y-hidden">{label}</span>
@@ -1,4 +0,0 @@
export { default as A11yIcon } from './a11y-icon.svelte';
export { default as Skip } from './skip.svelte';
export { default as Spinner } from './spinner.svelte';
export { default as Timestamp } from './timestamp.svelte';
@@ -1,156 +0,0 @@
<script>
/** @type {number} [size = 24] */
export let size = 24;
/** @type {number} */
export let value;
/** @type {'forward' | 'backward'} */
export let type;
</script>
<div class="svpod--skip-container" style="font-size:{size}px">
<span class="svpod--skip-label">
<span class="svpod--a11y-hidden">skip {type}</span>
{value}
<span class="svpod--a11y-hidden">seconds</span>
</span>
<span
class="svpod--skip-mask"
class:svpod--skip-mask--right={type === 'backward'}
class:svpod--skip-mask--left={type === 'backward'}
/>
<div class="svpod--skip-icon">
{#if type === 'forward'}
<!-- forward -->
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="font-size:{size}px"
>
<path
d="M15.5922 0.913446C14.1363 0.310389 12.5759 -1.8792e-08 11 0L11 2C12.3132 2 13.6136 2.25866 14.8268 2.7612C16.0401 3.26375 17.1425 4.00035 18.0711 4.92893C18.9997 5.85752 19.7362 6.95991 20.2388 8.17317C20.6133 9.07731 20.8524 10.0298 20.9499 11H22.9583C22.8551 9.76677 22.5617 8.55484 22.0866 7.4078C21.4835 5.95189 20.5996 4.62902 19.4853 3.51472C18.371 2.40042 17.0481 1.5165 15.5922 0.913446Z"
fill="currentColor"
/>
<path
d="M22.9583 13C22.9338 13.2923 22.8987 13.5835 22.853 13.8728L20.9082 13.3517C20.9242 13.2348 20.9381 13.1175 20.9499 13H22.9583Z"
fill="currentColor"
/>
<path
d="M20.4408 15.297L22.3771 15.8159C22.2894 16.0775 22.1925 16.3365 22.0866 16.5922C22.078 16.6128 22.0694 16.6334 22.0608 16.6539L20.3123 15.6444C20.3573 15.5293 20.4002 15.4135 20.4408 15.297Z"
fill="currentColor"
/>
<path
d="M19.3994 17.4268L21.1333 18.4278C20.9675 18.6892 20.7916 18.944 20.6061 19.1919L19.1744 17.7602C19.2516 17.6505 19.3267 17.5394 19.3994 17.4268Z"
fill="currentColor"
/>
<path
d="M17.8609 19.2751L19.2757 20.6899C19.0399 20.9144 18.7954 21.1289 18.5429 21.333L17.5281 19.5752C17.6412 19.4778 17.7521 19.3777 17.8609 19.2751Z"
fill="currentColor"
/>
<path
d="M15.8834 20.7265L16.8835 22.4587C16.5838 22.6273 16.2766 22.7831 15.9628 22.9257L15.4366 20.9619C15.5876 20.8872 15.7366 20.8087 15.8834 20.7265Z"
fill="currentColor"
/>
<path
d="M13.5553 21.668L14.0729 23.5999C13.7189 23.6937 13.3609 23.7712 13 23.8322V21.798C13.1862 21.7599 13.3714 21.7166 13.5553 21.668Z"
fill="currentColor"
/>
</svg>
{:else}
<!-- back -->
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="font-size:{size}px"
>
<path
d="M11.0003 21.9499V23.9583C10.6215 23.9266 10.2447 23.877 9.87115 23.8096L10.391 21.8697C10.5931 21.9026 10.7963 21.9294 11.0003 21.9499Z"
fill="currentColor"
/>
<path
d="M7.8915 21.1169C7.98478 21.159 8.07876 21.1996 8.17342 21.2388C8.26813 21.278 8.36337 21.3158 8.45911 21.352L7.9393 23.292C7.7608 23.2278 7.58367 23.1593 7.40805 23.0866C7.23249 23.0138 7.05886 22.937 6.8873 22.8562L7.8915 21.1169Z"
fill="currentColor"
/>
<path
d="M5.67173 19.7427C5.8305 19.8725 5.99315 19.9973 6.15943 20.1169L5.15524 21.8563C4.84345 21.6397 4.54188 21.4084 4.25159 21.1629L5.67173 19.7427Z"
fill="currentColor"
/>
<path
d="M3.88341 17.841C4.00303 18.0072 4.12778 18.1698 4.25752 18.3285L2.83737 19.7487C2.59192 19.4584 2.36059 19.1569 2.14409 18.8452L3.88341 17.841Z"
fill="currentColor"
/>
<path
d="M2.64825 15.5412C2.68449 15.6369 2.72223 15.7321 2.76146 15.8268C2.80069 15.9215 2.84134 16.0156 2.88341 16.1089L1.14409 17.1131C1.06327 16.9415 0.986441 16.7678 0.913699 16.5922C0.840963 16.4166 0.772485 16.2395 0.708295 16.061L2.64825 15.5412Z"
fill="currentColor"
/>
<path
d="M2.05038 13C2.07088 13.204 2.09764 13.4072 2.1306 13.6093L0.19065 14.1291C0.123306 13.7556 0.0736704 13.3788 0.0419922 13H2.05038Z"
fill="currentColor"
/>
<path
d="M2.05038 11C2.14788 10.0298 2.38695 9.07731 2.76146 8.17317C3.26401 6.95991 4.0006 5.85752 4.92919 4.92893C5.85777 4.00035 6.96016 3.26375 8.17342 2.7612C9.38667 2.25866 10.687 2 12.0003 2V0C10.4244 1.8792e-08 8.86396 0.310389 7.40805 0.913446C5.95215 1.5165 4.62928 2.40042 3.51497 3.51472C2.40067 4.62902 1.51676 5.95189 0.913699 7.4078C0.438578 8.55484 0.145121 9.76677 0.0419922 11H2.05038Z"
fill="currentColor"
/>
</svg>
{/if}
</div>
</div>
<style>
div.svpod--skip-container {
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
width: 1.5em;
height: 1.5em;
}
div.svpod--skip-icon,
span.svpod--skip-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
span.svpod--skip-mask {
z-index: 1;
background: linear-gradient(0deg, var(--bg), transparent);
opacity: 0.5;
}
div.svpod--skip-icon {
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--svpod--content--base);
}
span.svpod--skip-label {
position: relative;
z-index: 2;
font-size: 0.5em;
line-height: 1em;
color: var(--svpod--content--lighter);
}
@keyframes Spin {
0%,
100% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
@@ -1,22 +0,0 @@
<script>
import { seconds_to_timestamp } from 'svelte-podcast/utility';
/** @type {number} */
export let value;
/** @type {boolean} */
export let force_hours = false;
$: timestamp = seconds_to_timestamp(value, force_hours);
</script>
<span style="width:{timestamp.length}ch">{timestamp}</span>
<style>
span {
letter-spacing: 0.5px;
font-size: 0.8em;
display: block;
width: 8ch;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
export { default as Pause } from '@inqling/svelte-icons/heroicon-20-solid/pause.svelte';
export { default as Play } from '@inqling/svelte-icons/heroicon-20-solid/play.svelte';
export { default as SkipBack } from './skip-backward.svelte';
export { default as SkipForward } from './skip-forward.svelte';
export { default as LoadingSpinner } from './spinner.svelte';
+42
View File
@@ -0,0 +1,42 @@
<script>
/** @type {number} [size = 24] */
export let size = 24;
</script>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="font-size:{size}px"
>
<path
d="M11.0003 21.9499V23.9583C10.6215 23.9266 10.2447 23.877 9.87115 23.8096L10.391 21.8697C10.5931 21.9026 10.7963 21.9294 11.0003 21.9499Z"
fill="currentColor"
/>
<path
d="M7.8915 21.1169C7.98478 21.159 8.07876 21.1996 8.17342 21.2388C8.26813 21.278 8.36337 21.3158 8.45911 21.352L7.9393 23.292C7.7608 23.2278 7.58367 23.1593 7.40805 23.0866C7.23249 23.0138 7.05886 22.937 6.8873 22.8562L7.8915 21.1169Z"
fill="currentColor"
/>
<path
d="M5.67173 19.7427C5.8305 19.8725 5.99315 19.9973 6.15943 20.1169L5.15524 21.8563C4.84345 21.6397 4.54188 21.4084 4.25159 21.1629L5.67173 19.7427Z"
fill="currentColor"
/>
<path
d="M3.88341 17.841C4.00303 18.0072 4.12778 18.1698 4.25752 18.3285L2.83737 19.7487C2.59192 19.4584 2.36059 19.1569 2.14409 18.8452L3.88341 17.841Z"
fill="currentColor"
/>
<path
d="M2.64825 15.5412C2.68449 15.6369 2.72223 15.7321 2.76146 15.8268C2.80069 15.9215 2.84134 16.0156 2.88341 16.1089L1.14409 17.1131C1.06327 16.9415 0.986441 16.7678 0.913699 16.5922C0.840963 16.4166 0.772485 16.2395 0.708295 16.061L2.64825 15.5412Z"
fill="currentColor"
/>
<path
d="M2.05038 13C2.07088 13.204 2.09764 13.4072 2.1306 13.6093L0.19065 14.1291C0.123306 13.7556 0.0736704 13.3788 0.0419922 13H2.05038Z"
fill="currentColor"
/>
<path
d="M2.05038 11C2.14788 10.0298 2.38695 9.07731 2.76146 8.17317C3.26401 6.95991 4.0006 5.85752 4.92919 4.92893C5.85777 4.00035 6.96016 3.26375 8.17342 2.7612C9.38667 2.25866 10.687 2 12.0003 2V0C10.4244 1.8792e-08 8.86396 0.310389 7.40805 0.913446C5.95215 1.5165 4.62928 2.40042 3.51497 3.51472C2.40067 4.62902 1.51676 5.95189 0.913699 7.4078C0.438578 8.55484 0.145121 9.76677 0.0419922 11H2.05038Z"
fill="currentColor"
/>
</svg>
+42
View File
@@ -0,0 +1,42 @@
<script>
/** @type {number} [size = 24] */
export let size = 24;
</script>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="font-size:{size}px"
>
<path
d="M15.5922 0.913446C14.1363 0.310389 12.5759 -1.8792e-08 11 0L11 2C12.3132 2 13.6136 2.25866 14.8268 2.7612C16.0401 3.26375 17.1425 4.00035 18.0711 4.92893C18.9997 5.85752 19.7362 6.95991 20.2388 8.17317C20.6133 9.07731 20.8524 10.0298 20.9499 11H22.9583C22.8551 9.76677 22.5617 8.55484 22.0866 7.4078C21.4835 5.95189 20.5996 4.62902 19.4853 3.51472C18.371 2.40042 17.0481 1.5165 15.5922 0.913446Z"
fill="currentColor"
/>
<path
d="M22.9583 13C22.9338 13.2923 22.8987 13.5835 22.853 13.8728L20.9082 13.3517C20.9242 13.2348 20.9381 13.1175 20.9499 13H22.9583Z"
fill="currentColor"
/>
<path
d="M20.4408 15.297L22.3771 15.8159C22.2894 16.0775 22.1925 16.3365 22.0866 16.5922C22.078 16.6128 22.0694 16.6334 22.0608 16.6539L20.3123 15.6444C20.3573 15.5293 20.4002 15.4135 20.4408 15.297Z"
fill="currentColor"
/>
<path
d="M19.3994 17.4268L21.1333 18.4278C20.9675 18.6892 20.7916 18.944 20.6061 19.1919L19.1744 17.7602C19.2516 17.6505 19.3267 17.5394 19.3994 17.4268Z"
fill="currentColor"
/>
<path
d="M17.8609 19.2751L19.2757 20.6899C19.0399 20.9144 18.7954 21.1289 18.5429 21.333L17.5281 19.5752C17.6412 19.4778 17.7521 19.3777 17.8609 19.2751Z"
fill="currentColor"
/>
<path
d="M15.8834 20.7265L16.8835 22.4587C16.5838 22.6273 16.2766 22.7831 15.9628 22.9257L15.4366 20.9619C15.5876 20.8872 15.7366 20.8087 15.8834 20.7265Z"
fill="currentColor"
/>
<path
d="M13.5553 21.668L14.0729 23.5999C13.7189 23.6937 13.3609 23.7712 13 23.8322V21.798C13.1862 21.7599 13.3714 21.7166 13.5553 21.668Z"
fill="currentColor"
/>
</svg>
+315
View File
@@ -0,0 +1,315 @@
<script>
import {
LoadingSpinner,
Pause,
Play,
SkipBack,
SkipForward,
} from '$src/components/icon';
import { AudioPlayer, user_preferences } from 'svelte-podcast';
/** @type {string | undefined} */
export let src;
export let skip_back = 30;
export let skip_forward = 10;
/** @type {import('svelte-podcast/audio').AudioMetadata} */
export let metadata = {};
export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
</script>
<svelte:head>
<style>
:root {
--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);
}
</style>
</svelte:head>
<AudioPlayer {src} {metadata} let:Player let:action let:attributes>
<div
class="svpod-container {$$restProps.class}"
data-loaded={attributes.is_loaded ? 'true' : 'false'}
>
<!-- Player Action: Skip back -->
<button
on:click={() => action.skip_back(skip_back)}
class="svpod-skip"
type="button"
aria-label="Skip back {skip_back} seconds"
>
<div class="svpod-skip-label">
<span>
{skip_back}
</span>
</div>
<div class="svpod-skip-icon">
<SkipBack />
</div>
</button>
<!-- Player Action: Toggle play / pause -->
{#if attributes.is_loaded}
<button
on:click={() => action.toggle()}
class="svpod-toggle"
type="button"
aria-label={attributes.is_paused ? 'Play' : 'Pause'}
>
<svelte:component this={attributes.is_paused ? Play : Pause} />
</button>
{:else}
<div class="svpod-toggle">
<LoadingSpinner />
<span class="svpod--a11y">Waiting for audio...</span>
</div>
{/if}
<!-- Player Action: Skip forward -->
<button
on:click={() => action.skip_forward(skip_forward)}
class="svpod-skip"
type="button"
aria-label="Skip forward {skip_forward} seconds"
>
<div class="svpod-skip-label">
<span>
{skip_forward}
</span>
</div>
<div class="svpod-skip-icon">
<SkipForward />
</div>
</button>
<!-- Player Data: Current timestamp -->
<div class="svpod-timestamp" class:active={!attributes.is_paused}>
<span style="width:{attributes.timestamp_end.length}ch">
{attributes.timestamp_current}
</span>
</div>
<!-- Player Component: Timeline -->
<div class="svpod--timeline">
<Player.Progress />
</div>
<!-- Player Data: Total duration timestamp -->
<div class="svpod-timestamp">
<span style="width:{attributes.timestamp_end.length}ch">
{attributes.timestamp_end}
</span>
</div>
<!-- Player Action: Select playback speed -->
<select
class="svpod-playback"
value={$user_preferences.playback_rate}
on:change={(e) => {
const value = parseFloat(e.currentTarget.value);
action.set_playback_rate(value);
}}
>
{#each playback_rate_values as value}
<option {value}>
{#if Number.isInteger(value)}
{value}.0
{:else}
{value}
{/if}
</option>
{/each}
</select>
</div>
</AudioPlayer>
<style lang="postcss">
* {
/* css-reset */
text-transform: none;
font-style: normal;
text-indent: 0;
text-shadow: none;
text-align: left;
margin: 0;
padding: 0;
}
.svpod--a11y {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.svpod-container {
--size: 48px;
--column: var(--size);
--row: var(--size);
--text: calc(var(--size) / 3);
--padding: 2px;
--radius: 12px;
--inner-radius: calc(var(--radius) - var(--padding));
--bg: var(--svpod-surface-darker);
--fg: var(--svpod-content-base);
display: grid;
grid-template-columns:
var(--column)
var(--column)
var(--column)
max-content
auto
max-content
var(--column);
grid-template-rows: var(--column);
user-select: none;
background-color: var(--bg);
padding: var(--padding);
gap: var(--padding);
border-radius: var(--radius);
}
button,
select {
background-color: var(--bg);
color: var(--fg);
border-radius: var(--inner-radius);
overflow: hidden;
font-size: 0.8em;
font-weight: 500;
&:hover {
--fg: var(--svpod-content-lighter);
--bg: var(--svpod-surface-base);
}
&:focus {
outline: 2px solid var(--svpod-accent-lighter);
box-shadow: none;
}
}
.svpod-skip {
position: relative;
display: grid;
align-content: stretch;
justify-content: stretch;
.svpod-skip-icon {
position: absolute;
inset: 4px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
:global(svg) {
width: 100%;
height: 100%;
}
}
.svpod-skip-label {
width: 100%;
height: 100%;
display: grid;
place-content: center;
z-index: 30;
background: linear-gradient(0deg, var(--bg), transparent);
span {
line-height: 1em;
}
}
}
.svpod-toggle {
--fg: var(--svpod-content-lighter);
display: grid;
place-content: center;
&:hover {
--fg: var(--svpod-accent-lighter);
--bg: var(--svpod-accent-darker);
}
}
.svpod-playback {
appearance: none;
background-image: none;
margin: 0;
text-align: center;
text-align-last: center;
padding: 0 0.5em;
letter-spacing: 0.05em;
border: none;
&::before,
&::after {
content: none;
}
}
.svpod-timestamp {
display: grid;
place-content: center;
letter-spacing: 0.05em;
font-size: 0.8em;
text-align: center;
padding: 0.25em 1em;
color: var(--fg);
background-color: var(--bg);
border-radius: var(--inner-radius);
place-self: center;
span {
display: inline-block;
text-align: center;
}
&.active {
--fg: var(--svpod-accent-lighter);
--bg: var(--svpod-accent-darker);
}
}
.svpod--timeline {
display: flex;
align-items: center;
justify-content: stretch;
flex-grow: 1;
flex-shrink: 1;
font-size: 1em;
background: none;
--progress-height: var(--size);
--progress-fg: var(--svpod-content-base);
--progress-bg: var(--svpod-surface-base);
--progress-border: var(--svpod-surface-base);
--progress-active-fg: var(--svpod-accent-lighter);
--progress-active-bg: var(--svpod-accent-darker);
--progress-active-border: var(--svpod-accent-base);
}
</style>
+4 -3
View File
@@ -2,7 +2,7 @@
import { assets } from '$app/paths';
import { page } from '$app/stores';
import clsx from 'clsx';
import { circIn, circOut } from 'svelte/easing';
import { circOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
/**
@@ -77,12 +77,13 @@
x: -320,
duration: 300,
easing: circOut,
opacity: 0,
opacity: 1,
}}
out:fly={{
x: -320,
duration: 300,
easing: circIn,
easing: circOut,
opacity: 1,
}}
class={clsx(
'pointer-events-auto flex grow origin-left flex-col gap-y-5 overflow-y-auto bg-white px-6',
+1 -1
View File
@@ -46,7 +46,7 @@
</script>
<slot
Player={{ AudioProgress }}
Player={{ Progress: AudioProgress }}
action={{
set_playback_rate: user_preferences.set_playback_rate,
set_volume: user_preferences.set_volume,
+143
View File
@@ -68,3 +68,146 @@
readonly
/>
{/if}
<svelte:head>
<style>
:root {
/* initial player progress colors */
--progress-bg: rgb(0, 0, 0);
--progress-fg: rgb(155, 155, 155);
--progress-border: rgb(105, 105, 105);
--progress-active-bg: rgb(55, 55, 55);
--progress-active-fg: rgb(255, 255, 255);
--progress-active-border: rgb(155, 155, 155);
/* initial player progress shape */
--progress-radius: 4px;
--progress-thumb-width: 2px;
--progress-height: 40px;
}
</style>
</svelte:head>
<style lang="postcss">
input[type='range'] {
--thumb-border-offset: calc(var(--thumb-border-size) * 2);
--thumb-shape-height: calc(
var(--progress-height) - var(--thumb-border-offset)
);
--track-bg: var(--progress-bg);
--track-border-color: var(--progress-border);
--track-border-size: 1px;
--track-radius: var(--progress-radius);
--thumb--bg: var(--progress-fg);
--thumb-border-color: var(--progress-border);
--thumb-border-size: 0px;
--thumb--radius: var(--progress-radius);
--thumb-width: var(--progress-thumb-width);
font-size: 1em;
--borders: calc(var(--thumb-border-size) * 2);
height: calc(var(--thumb-shape-height) + var(--borders));
appearance: none;
margin: 0;
width: 100%;
background: transparent;
outline: 1px solid transparent;
&:hover {
--track-bg: var(--progress-active-bg);
--track-border-color: var(--progress-active-border);
--thumb--bg: var(--progress-active-fg);
--thumb-border-color: var(--progress-active-border);
}
&:focus {
outline-color: var(--progress-active-fg);
--track-border-color: var(--progress-active-fg);
--thumb--bg: var(--progress-active-fg);
--thumb-border-color: var(--progress-active-fg);
&::-webkit-slider-runnable-track,
&::-ms-fill-lower,
&::-ms-fill-upper {
background: var(--track-bg);
}
}
}
/* webkit */
input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: var(--progress-height);
cursor: pointer;
animate: 0.2s;
box-shadow: none;
background: var(--track-bg);
border-radius: var(--track-radius);
border: var(--track-border-size) solid var(--track-border-color);
}
input[type='range']::-webkit-slider-thumb {
--offset: calc(var(--track-border-size) * -1);
box-shadow: none;
border: var(--thumb-border-size) solid var(--thumb-border-color);
height: var(--thumb-shape-height);
width: var(--thumb-width);
border-radius: var(--thumb-radius);
background: var(--thumb--bg);
cursor: pointer;
-webkit-appearance: none;
margin-top: var(--offset);
}
/* mozilla */
input[type='range']::-moz-range-track {
width: 100%;
height: var(--progress-height);
cursor: pointer;
animate: 0.2s;
box-shadow: none;
background: var(--track-bg);
border-radius: var(--track-radius);
border: var(--track-border-size) solid var(--track-border-color);
}
input[type='range']::-moz-range-thumb {
box-shadow: none;
border: var(--thumb-border-size) solid var(--thumb-border-color);
height: var(--thumb-shape-height);
width: var(--thumb-width);
border-radius: var(--thumb-radius);
background: var(--thumb--bg);
cursor: pointer;
}
/* internet explorer */
input[type='range']::-ms-track {
width: 100%;
height: var(--progress-height);
cursor: pointer;
animate: 0.2s;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type='range']::-ms-fill-lower,
input[type='range']::-ms-fill-upper {
background: var(--track-bg);
border: var(--track-border-size) solid var(--track-border-color);
border-radius: calc(var(--track-radius) * 2);
box-shadow: none;
}
input[type='range']::-ms-thumb {
margin-top: 1px;
box-shadow: none;
border: var(--thumb-border-size) solid var(--thumb-border-color);
height: var(--thumb-shape-height);
width: var(--thumb-width);
border-radius: var(--thumb-radius);
background: var(--thumb--bg);
cursor: pointer;
}
</style>
+72 -36
View File
@@ -1,10 +1,13 @@
<script>
import { afterNavigate } from '$app/navigation';
import Sidebar from '$src/layout/sidebar.svelte';
import CloseIcon from '@inqling/svelte-icons/heroicon-24-outline/x-mark.svelte';
import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte';
import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte';
import { onMount } from 'svelte';
import github from 'svelte-highlight/styles/github';
import { circIn, circOut } from 'svelte/easing';
import { fade } from 'svelte/transition';
import { circOut, cubicInOut, cubicOut } from 'svelte/easing';
import { fade, scale } from 'svelte/transition';
import '../app.postcss';
const page_links = Object.freeze([
@@ -22,6 +25,25 @@
{ label: 'Type Safety', anchor: 'type_safety' },
],
},
{
label: 'Components',
href: '/components',
sections: [
{
label: 'AudioPlayer',
anchor: 'audioplayer',
},
{
label: 'AudioProgress',
anchor: 'audioprogress',
},
],
},
{
label: 'Examples',
href: '/examples',
sections: [],
},
{
label: 'API',
href: '/api',
@@ -67,11 +89,6 @@
{ label: 'seconds_to_timestamp', anchor: 'seconds_to_timestamp' },
],
},
{
label: 'Examples',
href: '/examples',
sections: [],
},
]);
const resource_links = Object.freeze([
@@ -104,6 +121,38 @@
const open_menu = () => (is_menu_open = true);
const close_menu = () => (is_menu_open = false);
const toggle_menu = () => (is_menu_open = !is_menu_open);
/**
* @param {KeyboardEvent} event - The keyboard event object.
*/
const handle_keydown = (event) => {
// on esc close menu
if (event.key === 'Escape' || event.key === 'Esc') {
close_menu();
}
// on command + / open menu
if (event.metaKey && event.key === '/') {
toggle_menu();
}
// on command + return open menu
if (event.metaKey && event.key === 'Enter') {
toggle_menu();
}
};
onMount(() => {
// close menu on esc
window.addEventListener('keydown', handle_keydown);
return () => {
window.removeEventListener('keydown', handle_keydown);
};
});
afterNavigate(close_menu);
</script>
<svelte:head>
@@ -119,46 +168,33 @@
{#if is_menu_open}
<div
in:fade={{
duration: 300,
easing: circOut,
duration: 400,
easing: cubicOut,
}}
out:fade={{
duration: 300,
easing: circIn,
duration: 120,
easing: cubicInOut,
}}
class="fixed inset-0 bg-mono-900/80"
/>
{/if}
<div class="fixed inset-0 flex">
{#if is_menu_open}
<div class="relative mr-16 flex w-full max-w-xs flex-1">
<div
in:fade
out:fade
class="absolute left-full top-0 flex w-16 justify-center pt-5"
>
<button
on:click={close_menu}
type="button"
class="pointer-events-auto -m-2.5 p-2.5"
on:click={close_menu}
class="pointer-events-auto fixed inset-0 flex"
>
<div class="relative mr-16 flex w-full max-w-xs flex-1">
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
<div
class="pointer-events-auto -m-2.5 origin-center p-2.5"
in:scale={{ delay: 200, easing: circOut, duration: 150 }}
out:scale={{ easing: cubicInOut, duration: 80 }}
>
<span class="sr-only">Close sidebar</span>
<svg
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<CloseIcon class="h-6 w-6 text-white" />
</div>
</div>
<Sidebar
@@ -168,8 +204,8 @@
podcasts={podcast_links}
/>
</div>
</button>
{/if}
</div>
</div>
<!-- Static sidebar for desktop -->
+1 -1
View File
@@ -1,6 +1,6 @@
<script>
import { base } from '$app/paths';
import { PlayerWidget } from '$src/components/example-player';
import { PlayerWidget } from '$src/components/player';
import { episodes } from '$src/content/episodes';
import { DocsPage } from '$src/layout/page';
+38
View File
@@ -0,0 +1,38 @@
<script>
import { episodes } from '$src/content/episodes';
import { DocsPage } from '$src/layout/page';
import { HighlightSvelte } from 'svelte-highlight';
import { PlayerCss, ProgressCss, player_css, progress_css } from './code';
/** @type { string | undefined} */
let audio_src = episodes.knomii.src;
audio_src;
</script>
<DocsPage title="Examples" let:Section>
<Section title="AudioPlayer">
<p>
A component that does not render any UI. Instead, it provides
convenient functions, values, and headless components via expoded
values
</p>
<p>This component should help</p>
<div class="not-prose py-4">
<PlayerCss />
</div>
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
<HighlightSvelte code={player_css} />
</div>
</Section>
<Section title="AudioProgress">
<div class="not-prose py-4">
<ProgressCss />
</div>
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
<HighlightSvelte code={progress_css} />
</div>
</Section>
</DocsPage>
@@ -0,0 +1,21 @@
<script>
// @ts-nocheck
import { AudioPlayer } from 'svelte-podcast';
/** @type { string | undefined} */
let src = '/static/example-syntax.mp3';
</script>
<AudioPlayer
src="/static/example-syntax.mp3"
metadata={{
title: `Supper Club × Rich Harris, Author of Svelte`,
artwork: `https://ssl-static.libsyn.com/p/assets/b/3/c/d/b3cdf28da11ad39fe5bbc093207a2619/Syntax_-_499.jpg`,
}}
let:Player
let:action
let:attributes
>
<pre>{JSON.stringify({ attributes }, null, 3)}</pre>
</AudioPlayer>
@@ -0,0 +1,5 @@
<script>
import { AudioProgress } from 'svelte-podcast/audio';
</script>
<AudioProgress step={10} />
+8
View File
@@ -0,0 +1,8 @@
import { format_code } from '$src/layout/helper';
import { default as player_css_src } from './audio-player.svelte?raw';
import { default as progress_css_src } from './audio-progress.svelte?raw';
export { default as PlayerCss } from './audio-player.svelte';
export { default as ProgressCss } from './audio-progress.svelte';
export const player_css = format_code(player_css_src);
export const progress_css = format_code(progress_css_src);
+18 -54
View File
@@ -1,69 +1,33 @@
<script>
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
import PlayerWidget from '$src/components/player/player-widget.svelte';
import { episodes } from '$src/content/episodes';
import { DocsPage } from '$src/layout/page';
import PreviewComponent from '$src/layout/preview-component.svelte';
$: player_widget = {
current_time: true,
playback_rate: true,
duration: true,
skip_back: 10,
skip_forward: 30,
};
import { HighlightSvelte } from 'svelte-highlight';
import { player_css } from './code';
/** @type { string | undefined} */
let audio_src = episodes.knomii.src;
</script>
<DocsPage title="Examples" let:Section>
<p class="text-xl font-semibold text-red-500">Work in progress!</p>
<Section title="Coming Soon...">
<p class="text-xl font-semibold text-red-500">
This page is a work in progress.
</p>
</Section>
<Section title="PlayerWidget">
<PreviewComponent name="PlayerWidget">
<svelte:fragment slot="options">
<div>
<label for="pw_current_time">current_time</label>
<input
id="pw_current_time"
type="checkbox"
bind:checked={player_widget.current_time}
<div class="not-prose py-4">
<PlayerWidget
src={audio_src}
skip_back={30}
skip_forward={10}
metadata={{}}
playback_rate_values={[1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4]}
/>
</div>
<div>
<label for="pw_playback_rate">playback_rate</label>
<input
id="pw_playback_rate"
type="checkbox"
bind:checked={player_widget.playback_rate}
/>
</div>
<div>
<label for="pw_duration">duration</label>
<input
id="pw_duration"
type="checkbox"
bind:checked={player_widget.duration}
/>
</div>
<div>
<label for="pw_skip_back">skip_back</label>
<input
id="pw_skip_back"
type="number"
bind:value={player_widget.skip_back}
/>
</div>
<div>
<label for="pw_skip_forward">skip_forward</label>
<input
id="pw_skip_forward"
type="number"
bind:value={player_widget.skip_forward}
/>
</div>
</svelte:fragment>
<PlayerWidget src={audio_src} include={player_widget} />
</PreviewComponent>
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
<HighlightSvelte code={player_css} />
</div>
</Section>
</DocsPage>
+4
View File
@@ -0,0 +1,4 @@
import { default as player_css_src } from '$src/components/player/player-widget.svelte?raw';
import { format_code } from '$src/layout/helper';
export const player_css = format_code(player_css_src);