Update Docs Site (#44)

This commit is contained in:
Ollie Taylor
2023-07-11 22:13:18 +01:00
committed by GitHub
parent 3cb5e8eb46
commit 29ef65733e
80 changed files with 1624 additions and 2381 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'svelte-podcast': minor
---
switch to jsdoc from ts
+6 -1
View File
@@ -32,7 +32,12 @@ module.exports = {
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [ 'unused-imports/no-unused-vars': [
'warn', 'warn',
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }, {
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
], ],
}, },
}; };
-1
View File
@@ -1,5 +1,4 @@
{ {
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"arrowParens": "always", "arrowParens": "always",
"bracketSpacing": true, "bracketSpacing": true,
+2 -14
View File
@@ -60,33 +60,21 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "^11.8.0",
"postcss": "^8.4.25", "postcss": "^8.4.25",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"prettier-plugin-tailwindcss": "^0.3.0", "prettier-plugin-tailwindcss": "^0.3.0",
"publint": "^0.1.16", "publint": "^0.1.16",
"rehype-add-classes": "^1.0.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-external-links": "^2.1.0",
"rehype-highlight": "^6.0.0",
"rehype-sanitize": "^5.0.1",
"rehype-slug": "^5.1.0",
"rehype-stringify": "^9.0.3",
"remark-gfm": "^3.0.1",
"remark-github": "^11.2.4",
"remark-parse": "^10.0.2",
"remark-rehype": "^10.1.0",
"remark-toc": "^8.0.1",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-check": "^3.4.5", "svelte-check": "^3.4.5",
"svelte-highlight": "^7.3.0",
"svelte-meta-tags": "^2.8.0",
"svelte-preprocess": "^5.0.4", "svelte-preprocess": "^5.0.4",
"svhighlight": "^0.7.1", "svhighlight": "^0.7.1",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"unified": "^10.1.2",
"vite": "^4.4.2" "vite": "^4.4.2"
}, },
"svelte": "./dist/index.js", "svelte": "./dist/index.js",
+4
View File
@@ -15,4 +15,8 @@ declare global {
} }
} }
declare module '*.md' {
export default ComponentType<SvelteComponentTyped>;
}
export {}; export {};
+10 -3
View File
@@ -1,12 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="scroll-smooth hover:scroll-auto"> <html lang="en" class="h-full scroll-smooth bg-white hover:scroll-auto">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="%sveltekit.assets%/icon.png" />
<meta name="theme-color" content="#f97316" />
<meta name="color-scheme" content="light" />
<link rel="bookmark" title="svelte-podcast Docs" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body class="scroll-smooth hover:scroll-auto"> <body class="h-full scroll-smooth hover:scroll-auto">
<div>%sveltekit.body%</div> <div>%sveltekit.body%</div>
</body> </body>
</html> </html>
+101 -169
View File
@@ -16,36 +16,14 @@
} }
@layer components { @layer components {
.prose code { .richtext h1 a,
@apply inline rounded bg-primary-100 px-2 py-1 font-mono leading-none text-primary-800; .richtext h2 a,
overflow-wrap: anywhere; .richtext h3 a,
} .richtext h4 a,
.prose code::before, .richtext h5 a,
.prose code::after { .richtext h6 a {
content: ''; @apply relative underline decoration-transparent decoration-solid underline-offset-2;
}
h1.anchor,
h2.anchor,
h3.anchor,
h4.anchor,
h5.anchor,
h6.anchor {
@apply m-0 text-3xl font-semibold leading-10 text-mono-900;
}
.prose h1 a,
.prose h2 a,
.prose h3 a,
.prose h4 a,
.prose h5 a,
.prose h6 a,
h1.anchor a,
h2.anchor a,
h3.anchor a,
h4.anchor a,
h5.anchor a,
h6.anchor a {
@apply relative font-medium underline decoration-transparent decoration-solid decoration-2 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;
content: '#'; content: '#';
@@ -58,168 +36,122 @@
} }
} }
.prose h3 a { .richtext code.hljs {
@apply text-primary-600; @apply p-0 text-sm;
overflow-wrap: anywhere;
} }
.prose #table-of-contents + ol { .richtext .codeblock code.hljs {
list-style: decimal-leading-zero; @apply overflow-hidden rounded-xl border border-mono-200 p-4 font-mono text-base leading-normal shadow-2xl shadow-mono-200;
}
.prose #table-of-contents + ol,
.prose #table-of-contents + ul {
ul,
ol,
li {
@apply my-0.5;
} }
ol { .richtext {
list-style: lower-alpha; @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;
ol {
list-style: disc;
ol {
list-style: circle;
}
}
}
} }
.prose #features + table td:first-child > * { .richtext td,
@apply sm:block sm:w-max; .richtext th {
} @apply px-4 py-2;
} }
/***** prism-theme *****/ .richtext td {
@apply min-w-max;
}
/* HLJS */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
/* /*
* Laserwave Theme originally by Jared Jones for Visual Studio Code Theme: GitHub
* https://github.com/Jaredk3nt/laserwave Description: Light theme as seen on github.com
* Author: github.com
* Ported for PrismJS by Simon Jespersen [https://github.com/simjes] Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/ */
.hljs {
code[class*='language-'], color: #24292e;
pre[class*='language-'] { background: #fff;
background: #27212e;
color: #ffffff;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; /* this is the default */
/* The following properties are standard, please leave them as they are */
font-size: 1em;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
/* The following properties are also standard */
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
} }
.hljs-doctag,
code[class*='language-']::-moz-selection, .hljs-keyword,
code[class*='language-'] ::-moz-selection, .hljs-meta .hljs-keyword,
pre[class*='language-']::-moz-selection, .hljs-template-tag,
pre[class*='language-'] ::-moz-selection { .hljs-template-variable,
background: #eb64b927; .hljs-type,
color: inherit; .hljs-variable.language_ {
color: #d73a49;
} }
.hljs-title,
code[class*='language-']::selection, .hljs-title.class_,
code[class*='language-'] ::selection, .hljs-title.class_.inherited__,
pre[class*='language-']::selection, .hljs-title.function_ {
pre[class*='language-'] ::selection { color: #6f42c1;
background: #eb64b927;
color: inherit;
} }
.hljs-attr,
/* Properties specific to code blocks */ .hljs-attribute,
pre[class*='language-'] { .hljs-literal,
padding: 1em; /* this is standard */ .hljs-meta,
margin: 0.5em 0; /* this is the default */ .hljs-number,
overflow: auto; /* this is standard */ .hljs-operator,
border-radius: 0.5em; .hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id,
.hljs-variable {
color: #005cc5;
} }
.hljs-meta .hljs-string,
/* Properties specific to inline code */ .hljs-regexp,
:not(pre) > code[class*='language-'] { .hljs-string {
padding: 0.2em 0.3em; color: #032f62;
border-radius: 0.5rem;
white-space: normal; /* this is standard */
} }
.hljs-built_in,
.token.comment, .hljs-symbol {
.token.prolog, color: #e36209;
.token.cdata {
color: #91889b;
} }
.hljs-code,
.token.punctuation { .hljs-comment,
color: #7b6995; .hljs-formula {
color: #6a737d;
} }
.hljs-name,
.token.builtin, .hljs-quote,
.token.constant, .hljs-selector-pseudo,
.token.boolean { .hljs-selector-tag {
color: #ffe261; color: #22863a;
} }
.hljs-subst {
.token.number { color: #24292e;
color: #b381c5;
} }
.hljs-section {
.token.important, color: #005cc5;
.token.atrule, font-weight: 700;
.token.property,
.token.keyword {
color: #40b4c4;
} }
.hljs-bullet {
.token.doctype, color: #735c0f;
.token.operator,
.token.inserted,
.token.tag,
.token.class-name,
.token.symbol {
color: #74dfc4;
} }
.hljs-emphasis {
.token.attr-name, color: #24292e;
.token.function,
.token.deleted,
.token.selector {
color: #eb64b9;
}
.token.attr-value,
.token.regex,
.token.char,
.token.string {
color: #b4dce7;
}
.token.entity,
.token.url,
.token.variable {
color: #ffffff;
}
/* The following rules are pretty similar across themes, but feel free to adjust them */
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic; font-style: italic;
} }
.hljs-strong {
.token.entity { color: #24292e;
cursor: help; font-weight: 700;
}
.hljs-addition {
color: #22863a;
background-color: #f0fff4;
}
.hljs-deletion {
color: #b31d28;
background-color: #ffeef0;
} }
.token.namespace {
opacity: 0.7;
} }
@@ -1,3 +1,2 @@
export { default as HeadlessTimeline } from './headless-timeline.svelte';
export { default as PlayerStack } from './player-stack.svelte'; 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,29 +1,36 @@
<script lang="ts"> <script>
import { Pause, Play, SpeakerWave } from '@inqling/svelte-icons/heroicon-20-solid'; import {
import { clsx } from 'clsx'; Pause,
import { AudioPlayer, episode_progress, user_preferences } from 'svelte-podcast'; Play,
import type { EpisodeDetails } from 'svelte-podcast/audio/stores'; SpeakerWave,
} from '@inqling/svelte-icons/heroicon-20-solid';
import {
AudioPlayer,
episode_progress,
user_preferences,
} from 'svelte-podcast';
import { A11yIcon, Skip, Spinner, Timestamp } from './utility'; import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
export let src: string | undefined; /** @type {string | undefined} */
export let metadata: EpisodeDetails = {}; export let src;
export let hide_timestamps: boolean = false; /** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
export let hide_playback_rate: boolean = false; export let metadata = {};
export let skip_back: number = 30; export let hide_timestamps = false;
export let skip_forward: number = 10; export let hide_playback_rate = false;
export let playback_rate_values: number[] = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4]; export let skip_back = 30;
export let skip_forward = 10;
const { class: ClassName, ...rest } = $$restProps; 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> <AudioPlayer {src} {metadata} let:Player let:action let:episode>
<div <div
class={clsx('svpod--container svpod--reset', ClassName)} {...$$restProps}
class="svpod--container svpod--reset"
data-loaded={episode.is_loaded ? 'true' : 'false'} data-loaded={episode.is_loaded ? 'true' : 'false'}
{...rest}
> >
<div class="svpod--container--row svpod--row--controls"> <div class="svpod--container--row svpod--row--controls">
<button <button
@@ -129,7 +136,9 @@
--svpod--timeline-track--shape--radius: var(--inner-radius); --svpod--timeline-track--shape--radius: var(--inner-radius);
--svpod--timeline-track--shape--border: 2px; --svpod--timeline-track--shape--border: 2px;
--svpod--timeline-thumb--shape--height: var(--svpod--timeline-track--shape--height); --svpod--timeline-thumb--shape--height: var(
--svpod--timeline-track--shape--height
);
--svpod--timeline-thumb--shape--width: 3px; --svpod--timeline-thumb--shape--width: 3px;
--svpod--timeline-thumb--shape--radius: 1px; --svpod--timeline-thumb--shape--radius: 1px;
--svpod--timeline-thumb--shape--border: 0px; --svpod--timeline-thumb--shape--border: 0px;
@@ -299,7 +308,11 @@
height: 100%; height: 100%;
content: ''; content: '';
opacity: 0.5; opacity: 0.5;
background: linear-gradient(45deg, var(--svpod--accent--base), transparent); background: linear-gradient(
45deg,
var(--svpod--accent--base),
transparent
);
} }
span.svpod--aspect--square { span.svpod--aspect--square {
@@ -311,10 +324,15 @@
.svpod--timeline { .svpod--timeline {
:global(input[type='range']) { :global(input[type='range']) {
--track--shape--height: calc(var(--svpod--timeline-track--shape--height)); --track--shape--height: calc(
--thumb-border-offset: calc(var(--svpod--timeline-thumb--shape--border) * 2); var(--svpod--timeline-track--shape--height)
);
--thumb-border-offset: calc(
var(--svpod--timeline-thumb--shape--border) * 2
);
--thumb--shape--height: calc( --thumb--shape--height: calc(
var(--svpod--timeline-thumb--shape--height) - var(--thumb-border-offset) var(--svpod--timeline-thumb--shape--height) -
var(--thumb-border-offset)
); );
--svpod--timeline-track--bg: var(--svpod--surface--base); --svpod--timeline-track--bg: var(--svpod--surface--base);
@@ -1,32 +1,31 @@
<script lang="ts"> <script>
import { Pause, Play } from '@inqling/svelte-icons/heroicon-20-solid'; import { Pause, Play } from '@inqling/svelte-icons/heroicon-20-solid';
import { clsx } from 'clsx';
import { AudioPlayer, user_preferences } from 'svelte-podcast'; import { AudioPlayer, user_preferences } from 'svelte-podcast';
import type { EpisodeDetails } from 'svelte-podcast/audio/stores';
import { A11yIcon, Skip, Spinner, Timestamp } from './utility'; import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
export let src: string | undefined; /** @type {string | undefined} */
export let metadata: EpisodeDetails = {}; export let src;
export let hide_duration: boolean = false; /** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
export let hide_current_time: boolean = false; export let metadata = {};
export let hide_playback_rate: boolean = false;
export let hide_skip_back: boolean = false;
export let hide_skip_forward: boolean = false;
export let skip_back: number = 30; export let hide_duration = false;
export let skip_forward: number = 10; 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 playback_rate_values: number[] = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4]; export let skip_back = 30;
export let skip_forward = 10;
const { class: ClassName, ...rest } = $$restProps; 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> <AudioPlayer {src} {metadata} let:Player let:action let:episode>
<div <div
class={clsx('svpod--container svpod--reset', ClassName)} {...$$restProps}
class="svpod--container svpod--reset"
data-loaded={episode.is_loaded ? 'true' : 'false'} data-loaded={episode.is_loaded ? 'true' : 'false'}
{...rest}
> >
{#if !hide_skip_back} {#if !hide_skip_back}
<button <button
@@ -68,7 +67,10 @@
{#if !hide_current_time} {#if !hide_current_time}
<div class="svpod--timestamp"> <div class="svpod--timestamp">
<Timestamp value={episode.current_time} force_hours={episode.timestamp_hours} /> <Timestamp
value={episode.current_time}
force_hours={episode.timestamp_hours}
/>
</div> </div>
{/if} {/if}
@@ -118,7 +120,9 @@
--svpod--timeline-track--shape--radius: var(--inner-radius); --svpod--timeline-track--shape--radius: var(--inner-radius);
--svpod--timeline-track--shape--border: 2px; --svpod--timeline-track--shape--border: 2px;
--svpod--timeline-thumb--shape--height: var(--svpod--timeline-track--shape--height); --svpod--timeline-thumb--shape--height: var(
--svpod--timeline-track--shape--height
);
--svpod--timeline-thumb--shape--width: 3px; --svpod--timeline-thumb--shape--width: 3px;
--svpod--timeline-thumb--shape--radius: 1px; --svpod--timeline-thumb--shape--radius: 1px;
--svpod--timeline-thumb--shape--border: 0px; --svpod--timeline-thumb--shape--border: 0px;
@@ -226,10 +230,15 @@
.svpod--timeline { .svpod--timeline {
:global(input[type='range']) { :global(input[type='range']) {
--track--shape--height: calc(var(--svpod--timeline-track--shape--height)); --track--shape--height: calc(
--thumb-border-offset: calc(var(--svpod--timeline-thumb--shape--border) * 2); var(--svpod--timeline-track--shape--height)
);
--thumb-border-offset: calc(
var(--svpod--timeline-thumb--shape--border) * 2
);
--thumb--shape--height: calc( --thumb--shape--height: calc(
var(--svpod--timeline-thumb--shape--height) - var(--thumb-border-offset) var(--svpod--timeline-thumb--shape--height) -
var(--thumb-border-offset)
); );
--svpod--timeline-track--bg: var(--svpod--surface--base); --svpod--timeline-track--bg: var(--svpod--surface--base);
@@ -1,8 +1,9 @@
<script lang="ts"> <script>
import type { SvelteIcon } from '@inqling/svelte-icons'; /** @type {import('@inqling/svelte-icons').SvelteIcon} */
export let icon;
export let icon: SvelteIcon; /** @type {string} */
export let label: string; export let label;
</script> </script>
<svelte:component this={icon} /> <svelte:component this={icon} />
@@ -1,7 +1,12 @@
<script lang="ts"> <script>
/** @type {number} [size = 24] */
export let size = 24; export let size = 24;
export let value: number;
export let type: 'forward' | 'backward'; /** @type {number} */
export let value;
/** @type {'forward' | 'backward'} */
export let type;
</script> </script>
<div class="svpod--skip-container" style="font-size:{size}px"> <div class="svpod--skip-container" style="font-size:{size}px">
@@ -1,4 +1,5 @@
<script lang="ts"> <script>
/** @type {number} */
export let size = 24; export let size = 24;
</script> </script>
@@ -1,7 +1,10 @@
<script lang="ts"> <script>
import { seconds_to_timestamp } from 'svelte-podcast'; import { seconds_to_timestamp } from 'svelte-podcast';
export let value: number; /** @type {number} */
export let value;
/** @type {boolean} */
export let force_hours = false; export let force_hours = false;
$: timestamp = seconds_to_timestamp(value, force_hours); $: timestamp = seconds_to_timestamp(value, force_hours);
-6
View File
@@ -1,6 +0,0 @@
<script lang="ts">
export let code: string;
export let description: string;
</script>
<pre><code class="hljs language-js">{`//${description}\n\n${code}`}</code></pre>
-14
View File
@@ -1,14 +0,0 @@
<script lang="ts">
export let name: string;
export let data: unknown;
</script>
<div class="flex flex-col items-stretch space-y-6">
<h3>{name}</h3>
<h4>subscription: ${name}</h4>
<pre><code class="hljs language-json">{JSON.stringify({ data }, null, 2)}</code></pre>
<h4>methods</h4>
<slot />
</div>
-58
View File
@@ -1,58 +0,0 @@
# Hello World
this is some content from @OllieJT on pr #32
this is on https://github.com/OllieJT/svelte-podcast/pull/39
> this is a quote
## this is a subheading
`const this_is = "code"`
```js
const this_is = 'code_block';
console.log(this_is);
```
```js {1,3-4} showLineNumbers
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' });
}
}
```
```svelte {1,3-4} showLineNumbers
<script>
import { episode_audio } from 'svelte-podcast';
</script>
<!-- load the episode on click -->
<button
on:click={() =>
episode_audio.load('/episode-audio.mp3', {
/* optional metadata */
})}
>
Load Episode
</button>
<!-- unload the episode on click -->
<button on:click={() => episode_audio.unload()}>Unload Episode</button>
```
[This is a link](https://www.google.com)
- this is a list
- this is a list
- this is a list
1. this is a numbered list
2. this is a numbered list
3. this is a numbered list
| this | is | a | table |
| ---- | --- | --- | ----- |
| this | is | a | table |
| this | is | a | table |
-17
View File
@@ -1,17 +0,0 @@
<script lang="ts">
import { topics } from '$src/content/utility/anchor-registry';
import { Hashtag } from '@inqling/svelte-icons/heroicon-24-outline';
export let value: string;
$: id = topics.slugify(value || '');
topics.add(value);
</script>
<a href="/#{id}" class="group relative -ml-[1em] hover:text-black">
<Hashtag
class="h-[1em] w-[1em] -translate-x-1 text-mono-300 group-hover:text-primary-600"
/><span class="underline decoration-primary-50/0 decoration-4 group-hover:decoration-primary-500"
>{value}</span
>
<span class="pointer-events-none absolute -top-10 h-px w-px" {id} />
</a>
@@ -1,19 +0,0 @@
<script lang="ts">
import Anchor from './anchor.svelte';
export let title: string;
export let anchor: boolean = false;
</script>
<li class="list-item list-disc text-primary-800">
<h4 class="text-lg font-medium leading-normal text-primary-600">
{#if anchor}
<Anchor value={title} />
{:else}
{title}
{/if}
</h4>
<p class="mt-0 text-mono-600">
<slot />
</p>
</li>
@@ -1,11 +0,0 @@
<script lang="ts">
import Anchor from './anchor.svelte';
export let title: string;
export let subtitle: string | undefined = undefined;
</script>
<h3 class="text-xl font-medium leading-relaxed text-mono-900"><Anchor value={title} /></h3>
{#if subtitle}
<p class="text-xl font-normal leading-normal text-mono-500">{subtitle}</p>
{/if}
-8
View File
@@ -1,8 +0,0 @@
<script lang="ts">
</script>
<article
class="section-content my-4 w-full border-t-2 border-mono-100 py-4 text-mono-600 first-of-type:border-t-0 first-of-type:pt-0"
>
<slot />
</article>
-29
View File
@@ -1,29 +0,0 @@
<script lang="ts">
import type { SvelteIcon } from '@inqling/svelte-icons';
type Feature = {
icon: SvelteIcon;
title: string;
description: string;
};
export let features: Feature[];
</script>
<dl
class="mx-auto grid max-w-2xl grid-cols-1 gap-6 py-6 text-base leading-7 text-mono-600 sm:grid-cols-2 sm:gap-y-10 lg:mx-0 lg:max-w-none"
>
{#each features as feature}
<div class="relative pl-9">
<dt class="inline font-semibold text-mono-900">
<svelte:component
this={feature.icon}
class="absolute left-1 top-1 h-5 w-5 text-primary-600"
/>
{feature.title}
</dt>
<dd class="inline">{feature.description}</dd>
</div>
{/each}
</dl>
-6
View File
@@ -1,6 +0,0 @@
export { default as ContentListItem } from './content-list-item.svelte';
export { default as ContentTitle } from './content-title.svelte';
export { default as Content } from './content.svelte';
export { default as Features } from './features.svelte';
export { default as SectionTitle } from './section-title.svelte';
export { default as Section } from './section.svelte';
@@ -1,9 +0,0 @@
<script lang="ts">
import Anchor from './anchor.svelte';
export let title: string;
</script>
<h2 class="text-3xl font-normal leading-loose text-primary-600">
<Anchor value={title} />
</h2>
-9
View File
@@ -1,9 +0,0 @@
<script lang="ts">
</script>
<!-- <div class="relative mx-auto w-full max-w-7xl p-4 sm:px-6 lg:px-8"> -->
<div class="relative mx-auto w-full max-w-prose p-4 text-lg sm:px-6 lg:px-8">
<section class="w-full">
<slot />
</section>
</div>
-184
View File
@@ -1,184 +0,0 @@
## Table of contents
## What is svelte-podcast?
### Motivation
At it's core svelte-podcast provides tooling to make the following tasks easier:
#### I want to build a custom audio player
The default audio element is quite nice, but there are 2 main reasons why people want to build their own;
1. to customize the look and feel
2. to add custom functionality.
svelte-podcast provides a simple API enabling you to resolve both of these desires.
#### I find managing the state of audio difficult
It's one thing to load, play, and pause audio on a single page, but what if you want to build more custom or advanced behaviours like
> Can audio continue playing when the user navigates to a new page.
> Can I play/pause an episode from content like an article?
> Can I highlight show notes relevant to the current timestamp?
svelte-podcast makes solving these challenges easier by simplifying your interactions with the audio element.
#### I find RSS frustrating to work with
We're still working on this, it'll be coming soon™️!
### Features
| Feature | Description |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| **Simple API** | Get essential audio data like the current timestamp, duration, play state and more... |
| **Simple Controls** | Control your active audio source with play, pause, seek, and more methods... |
| **Extensible** | Define your own metadata for each episode to easily association content with the current audio |
| **Save Progress** | Save and load a users progress and preferences to localStorage, or your own database |
| **Persistence** | If your site makes use of client side routing, audio will continue playing when users navigate |
| **Typescript** | 1st class types, with type overrides for defining your own metadata requirements |
## Get Started
### Install
Install the latest version of svelte-podcast with your preferred package manager.
```sh
# with npm
npm install svelte-podcast@latest
# with yarn
yarn add svelte-podcast@latest
# with pnpm
pnpm add svelte-podcast@latest
```
### Setup
Add the AudioLoader component to your root layout. You must have one of these for svelte-podcast to work. You should also only load one instance of this at a time, and so we recommend you loading it at the base of your app.
```svelte
<!-- /routes/+layout.svelte -->
<script>
import { AudioLoader } from 'svelte-podcast';
</script>
<AudioLoader />
<!-- your layout -->
<slot />
```
### Load an episode
All you need to load an episode is a URL to an audio file. svelte-podcast uses a html audio element under the hood, so any audio file compatible with the autio element is also compatible with this package.
#### Using a URL (Most Common)
An **audio url** could be a URL to an MP3 file from an RSS feed, like this: `https://media.transistor.fm/27a058c9/27b595e2.mp3`. It could also be a path to a static file on your server.
#### Using a static file
If you're using SvelteKit, you can store **static files** in the /static directory. When your site is built, everything in the static directory will be at the root of your site. If you have a file in `/static/episides/episode-01.mp3` you could load it as `/episides/episode-01.mp3`
#### Using the AudioLoader component
Only one piece of audio can be loaded at a time, however a users progress for each audio is saved in localStorage when they play, pause, or skip. You can load audio via the `episode_audio` store from anywhere in your project.
##### Example: Load audio after page is loaded
```svelte
<script>
import { onMount } from 'svelte';
import { episode_audio } from 'svelte-podcast';
onMount(() => {
// load the episode on mount without any metadata
episode_audio.load('/episode-audio.mp3', {
/* optional metadata */
});
});
</script>
```
##### Example: Load audio after a user clicks a button
```svelte
<script>
import { episode_audio } from 'svelte-podcast';
</script>
<!-- load the episode on click -->
<button
on:click={() =>
episode_audio.load('/episode-audio.mp3', {
/* optional metadata */
})}
>
Load Episode
</button>
<!-- unload the episode on click -->
<button on:click={() => episode_audio.unload()}>Unload Episode</button>
```
## Components
### `<PlayerWidget/>`
Coming soon...
### `<PlayerStack/>`
Coming soon...
### `<HeadlessTimeline/>`
Coming soon...
## Utilities
### Debug Mode
Coming soon...
### audio
Coming soon...
### episode_progress
Coming soon...
### episode_details
Coming soon...
### user_progress & user_preferences
Coming soon...
### save_podcast_state
Coming soon...
### secondsToTimestamp
Coming soon...
## Typescript
### Override types
Coming soon...
### Exported types
Coming soon...
@@ -1,6 +1,11 @@
import { assets } from '$app/paths'; import { assets } from '$app/paths';
export const episodes = { /**
* Audio context module.
* @module episodes
*/
export const episodes = Object.freeze({
syntax: { syntax: {
src: `${assets}/example-syntax.mp3`, src: `${assets}/example-syntax.mp3`,
title: `Supper Club × Rich Harris, Author of Svelte`, title: `Supper Club × Rich Harris, Author of Svelte`,
@@ -11,4 +16,4 @@ export const episodes = {
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`,
}, },
} as const; });
-5
View File
@@ -1,5 +0,0 @@
import example_add_loader from './add-loader.svelte?raw';
import example_load_audio_click from './load-audio-click.svelte?raw';
import example_load_audio_mount from './load-audio-mount.svelte?raw';
export { example_add_loader, example_load_audio_click, example_load_audio_mount };
-37
View File
@@ -1,37 +0,0 @@
import { writable } from 'svelte/store';
type Topic = string;
const content = new Set<Topic>();
const ropic_registry = writable<Topic[]>([...content]);
function add(value: Topic) {
ropic_registry.update((prev) => {
const content = new Set<Topic>(prev.filter(Boolean));
Boolean(value) && content.add(value);
return [...content];
});
}
function slugify(value: string) {
// replace all apaces with dashes
const slug = value
.replace(/\s+/g, '-')
.toLowerCase()
.trim()
// eslint-disable-next-line no-control-regex
.replace(/[^\x00-\x7F]/g, '-')
.replace(/[^a-z0-9-]/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
.replace(/--+/g, '-');
return encodeURI(slug);
}
export const topics = {
subscribe: ropic_registry.subscribe,
add,
slugify,
};
-8
View File
@@ -1,8 +0,0 @@
import type { CodeBlockProps } from 'svhighlight/code/CodeBlock.svelte';
export const code_opts = {
background: 'bg-mono-900',
focusType: 'highlight',
highlightColor: 'bg-mono-800',
headerClasses: 'bg-mono-800',
} satisfies CodeBlockProps;
-8
View File
@@ -1,8 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// import type { ComponentType, SvelteComponentTyped } from 'svelte';
declare module '*.md' {
export default ComponentType<SvelteComponentTyped>;
}
+5
View File
@@ -0,0 +1,5 @@
export const slugify = (/** @type {string} */ str) =>
encodeURI(str.toLowerCase().trim().replaceAll(' ', '_'));
export const format_code = (/** @type {string} */ code) =>
code.replaceAll('\t', ' ');
+36
View File
@@ -0,0 +1,36 @@
<script>
import { base } from '$app/paths';
import { page } from '$app/stores';
import { MetaTags } from 'svelte-meta-tags';
const site_name = 'Svelte Podcast';
/** @type {string} */
export let title = 'The fastest way to build a podcast site with Svelte.';
/** @type {string} */
export let description =
'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} */
export let canonical = $page.url.href + base;
// TODO: add screenshots under images
/** @type {import('svelte-meta-tags').OpenGraph["images"]}*/
export let images = [];
</script>
<MetaTags
titleTemplate="%s | {site_name}"
{title}
{description}
{canonical}
openGraph={{
locale: 'en_US',
site_name,
type: 'website',
url: canonical,
title: title,
description,
images,
}}
/>
+26
View File
@@ -0,0 +1,26 @@
<script>
import { slugify } from '$src/layout/helper';
import Metadata from '$src/layout/metadata.svelte';
import Container from '$src/layout/page/container.svelte';
import SectionArticle from '$src/layout/page/section-article.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';
/** @type {string}*/
export let title;
export let anchor = slugify(title);
</script>
<Metadata {title} />
<slot name="header" {anchor} {title}>
<Container as="header" aria-labelledby={anchor}>
<div class="richtext pt-20">
<h1 id={anchor} class="my-0">{title}</h1>
</div>
</Container>
</slot>
<slot {Section} {SectionArticle} {TableModule} {TableSchema} />
+12
View File
@@ -0,0 +1,12 @@
<script>
/** @type {'header'|'section'|'div'}*/
export let as = 'div';
</script>
<svelte:element
this={as}
{...$$restProps}
class="relative mx-auto w-full max-w-6xl p-4 md:p-6"
>
<slot />
</svelte:element>
+1
View File
@@ -0,0 +1 @@
export { default as DocsPage } from './component.svelte';
+19
View File
@@ -0,0 +1,19 @@
<script>
import { slugify } from '$src/layout/helper';
/** @type {string} */
export let title;
export let anchor = slugify(title);
/** @type {'h3'|'h4'|'h5'} */
export let level = 'h3';
</script>
<article aria-labelledby={anchor}>
<svelte:element this={level} id={anchor}>
<a href="#{anchor}">{title}</a>
</svelte:element>
<slot />
</article>
+22
View File
@@ -0,0 +1,22 @@
<script>
import { slugify } from '$src/layout/helper';
import Container from '$src/layout/page/container.svelte';
/** @type {string} */
export let title;
export let anchor = slugify(title);
/** @type {'h2'|'h3'|'h4'|'h5'} */
export let level = 'h2';
</script>
<Container as="section" aria-labelledby={anchor}>
<div class="richtext">
<svelte:element this={level} id={anchor} class="mt-0">
<a href="#{anchor}">{title}</a>
</svelte:element>
<slot />
</div>
</Container>
+33
View File
@@ -0,0 +1,33 @@
<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>
+40
View File
@@ -0,0 +1,40 @@
<script>
import { Highlight } from 'svelte-highlight';
import lang_ts from 'svelte-highlight/languages/typescript';
/**
* Table Row
* @typedef {Object} Row
* @property {string} property - Property on the model
* @property {string} type - Value type
* @property {string} description - Description of the property
*/
/** @type {Row[]} */
export let rows;
</script>
<div class="w-full overflow-x-auto">
<table class="">
<thead>
<th class="w-max">Property</th>
<th class="w-max">Type</th>
<th>Description</th>
</thead>
<tbody>
{#each rows as { property, type, description }}
<tr>
<!-- <td class="pr-4 font-mono">will_autoplay</td> -->
<!-- <td class="pr-4 font-mono text-primary-600">boolean</td> -->
<td class="not-prose">
<Highlight code={property} language={lang_ts} />
</td>
<td class="not-prose">
<Highlight code={type} language={lang_ts} />
</td>
<td> {description} </td>
</tr>
{/each}
</tbody>
</table>
</div>
@@ -1,5 +1,6 @@
<script lang="ts"> <script>
export let name: string; /** @type {string} */
export let name;
</script> </script>
<div class="flex flex-col items-stretch space-y-6"> <div class="flex flex-col items-stretch space-y-6">
+179
View File
@@ -0,0 +1,179 @@
<script>
import { assets } from '$app/paths';
import { page } from '$app/stores';
import clsx from 'clsx';
import { circIn, circOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
/**
* Audio context module.
* @module sidebar
*/
/**
* @typedef {Object} SectionLink
* @property {string} label - The label of the page link.
* @property {string} anchor - The URL of the page link.
*/
/**
* @typedef {Object} PageLink
* @property {string} label - The label of the page link.
* @property {string} href - The URL of the page link.
* @property {SectionLink[]} sections - The sections of the page link.
*/
/** @type {Readonly<PageLink[]>} */
export let pages;
/**
* @typedef {Object} ResourceLink
* @property {string} label - The label of the page link.
* @property {string} href - The URL of the page link.
* @property {import('@inqling/svelte-icons').SvelteIcon} icon - The URL of the page link.
*/
/** @type {Readonly<ResourceLink[]>} */
export let resources;
$: use_is_current = (/** @type {string } */ href) => {
let href_pathname = href;
if (href.startsWith('http')) {
href_pathname = new URL(href).pathname;
}
// remove leading and training slashes
href_pathname = href_pathname.replace(/^\/|\/$/g, '');
const page_pathname = $page.url.pathname.replace(/^\/|\/$/g, '');
return page_pathname === href_pathname;
};
const use_is_current_section = (
/** @type {boolean} */ is_current_page,
/** @type {string} */ anchor,
) => {
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);
};
</script>
<!-- Sidebar component, swap this element with another sidebar if you like -->
<div
in:fly={{
x: -320,
duration: 300,
easing: circOut,
opacity: 0,
}}
out:fly={{
x: -320,
duration: 300,
easing: circIn,
}}
class={clsx(
'pointer-events-auto flex grow origin-left flex-col gap-y-5 overflow-y-auto bg-white px-6',
$$restProps.class,
)}
>
<a href="/" class="flex h-16 shrink-0 items-center">
<img
class="h-8 w-auto"
width={60}
height={32}
src="{assets}/logo.png"
alt="Svelte-Podcast"
/>
</a>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" class="-mx-2">
{#each pages as link}
{@const is_current = use_is_current(link.href)}
<li class="py-0.5">
<a
class:active={is_current}
href={link.href}
class={clsx(
'group flex gap-x-3 rounded-md px-3 py-1.5 text-sm font-semibold leading-6',
is_current
? 'bg-primary-50 text-primary-600'
: 'text-mono-600 hover:bg-mono-50 hover:text-mono-950',
)}
>
{link.label}
</a>
{#if link.sections.length}
<ul role="list" class="border-l border-mono-200 pl-3">
{#each link.sections as section}
{@const is_current_section =
use_is_current_section(
is_current,
section.anchor,
)}
{@const section_href = [
link.href,
section.anchor,
].join('#')}
<li class="py-0.5">
<a
class:active={is_current_section}
href={section_href}
class={clsx(
'group flex gap-x-3 rounded-md px-3 py-1.5 text-xs font-medium leading-6',
is_current_section
? 'text-primary-600'
: 'text-mono-500 hover:bg-mono-50 hover:text-mono-950',
)}
>
{section.label}
</a>
</li>
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
</li>
<li>
<div class="text-xs font-semibold leading-6 text-mono-400">
Resources
</div>
<ul role="list" class="-mx-2 mt-2 space-y-1">
{#each resources 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',
)}
>
<span
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"
>
<svelte:component this={link.icon} class="h-4 w-4" />
</span>
<span class="truncate">{link.label}</span>
</a>
</li>
{/each}
</ul>
</li>
</ul>
</nav>
</div>
+5 -4
View File
@@ -1,8 +1,8 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { announce } from '../internal';
import { user_preferences } from '../user'; import { user_preferences } from '../user';
import { announce } from '../utility';
import { audio_element } from './stores/audio-element'; import { audio_element } from './stores/audio-element';
$: $audio_element; $: $audio_element;
@@ -24,9 +24,10 @@
<svelte:head> <svelte:head>
<style> <style>
:root { :root {
--svpod--font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', --svpod--font: ui-sans-serif, system-ui, -apple-system,
Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; '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--darker: rgb(0, 0, 0);
--svpod--surface--base: rgb(40, 40, 40); --svpod--surface--base: rgb(40, 40, 40);
+3 -1
View File
@@ -17,7 +17,9 @@
$: is_playing = $episode_audio?.is_paused === false; $: is_playing = $episode_audio?.is_paused === false;
$: current_time = $episode_progress?.current_time || 0; $: current_time = $episode_progress?.current_time || 0;
$: duration = $episode_audio?.duration || 0; $: duration = $episode_audio?.duration || 0;
$: timestamp_hours = Boolean($episode_audio?.duration && $episode_audio.duration >= 3600); $: timestamp_hours = Boolean(
$episode_audio?.duration && $episode_audio.duration >= 3600,
);
$: timestamp = duration && seconds_to_timestamp(duration); $: timestamp = duration && seconds_to_timestamp(duration);
onMount(() => { onMount(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
<script> <script>
import { episode_audio, episode_progress } from '../audio'; import { episode_audio, episode_progress } from '../audio';
import { announce } from '../utility'; import { announce } from '../internal';
export let step = 10; export let step = 10;
+4
View File
@@ -39,6 +39,10 @@ export const audio_element = readable(undefined, (set) => {
if (!existing_element) document.body.appendChild(el); 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 // @ts-expect-error - I'm not sure how to fix this using JSDOC
set(el); set(el);
}); });
+8 -4
View File
@@ -1,7 +1,7 @@
import clamp from 'just-clamp'; import clamp from 'just-clamp';
import { derived, get } from 'svelte/store'; import { derived, get } from 'svelte/store';
import { user_preferences, user_progress } from '../../user'; import { user_preferences, user_progress } from '../../user';
import { announce } from '../../utility'; import { announce } from '../../internal';
import { audio_element } from './audio-element'; import { audio_element } from './audio-element';
import { episode_details } from './episode-details'; import { episode_details } from './episode-details';
@@ -29,7 +29,9 @@ const default_episode_state = {
* Episode state store * Episode state store
* @type {import('svelte/store').Readable<EpisodeState | null>} * @type {import('svelte/store').Readable<EpisodeState | null>}
*/ */
const episode_state = derived([audio_element, episode_details], ([$audio, $details], set) => { const episode_state = derived(
[audio_element, episode_details],
([$audio, $details], set) => {
if (!$audio) return set(null); if (!$audio) return set(null);
function set_value() { function set_value() {
@@ -54,7 +56,8 @@ const episode_state = derived([audio_element, episode_details], ([$audio, $detai
$audio.removeEventListener('pause', set_value); $audio.removeEventListener('pause', set_value);
$audio.removeEventListener('playing', set_value); $audio.removeEventListener('playing', set_value);
}; };
}); },
);
/** /**
* @typedef {'toggle' | 'set'} HANDLE_TYPE * @typedef {'toggle' | 'set'} HANDLE_TYPE
@@ -68,7 +71,8 @@ const episode_state = derived([audio_element, episode_details], ([$audio, $detai
*/ */
const use_element = (action) => { const use_element = (action) => {
const el = get(audio_element); const el = get(audio_element);
if (!el) throw announce.warn(`could not ${action} :: no audio element exists yet`); if (!el)
throw announce.warn(`could not ${action} :: no audio element exists yet`);
return el; return el;
}; };
+7
View File
@@ -0,0 +1,7 @@
/**
* Utility functions module.
* @module internal
*/
export * from './announce';
export * from './use-url';
+8 -2
View File
@@ -17,7 +17,10 @@ const DEFAULT_USER_PREFERENCES = { playback_rate: 1, volume: 1 };
* The user preferences store * The user preferences store
* @type {import('svelte/store').Writable<UserPreferences>} * @type {import('svelte/store').Writable<UserPreferences>}
*/ */
const USER_PREFERENCES_STORE = persisted('USER_PREFERENCE', DEFAULT_USER_PREFERENCES); const USER_PREFERENCES_STORE = persisted(
'USER_PREFERENCE',
DEFAULT_USER_PREFERENCES,
);
/** /**
* Sets the playback rate for the user * Sets the playback rate for the user
@@ -26,7 +29,10 @@ const USER_PREFERENCES_STORE = persisted('USER_PREFERENCE', DEFAULT_USER_PREFERE
*/ */
const set_playback_rate = (value) => { const set_playback_rate = (value) => {
const playback_rate = clamp(value, 0.5, 5); const playback_rate = clamp(value, 0.5, 5);
return USER_PREFERENCES_STORE.update((prefs) => ({ ...prefs, playback_rate })); return USER_PREFERENCES_STORE.update((prefs) => ({
...prefs,
playback_rate,
}));
}; };
/** /**
+7 -3
View File
@@ -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 { episode_audio, episode_progress } from '../audio';
import { announce, use_url } from '../utility'; import { use_url, announce } from '../internal';
/** /**
* User progress object type. * User progress object type.
@@ -34,7 +34,10 @@ const save_user_progress = () => {
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 = get(episode_progress).current_time;
USER_PROGRESS_STORE.update((prev) => ({ ...prev, [pathname]: current_time })); USER_PROGRESS_STORE.update((prev) => ({
...prev,
[pathname]: current_time,
}));
}; };
/** /**
@@ -59,7 +62,8 @@ const get_user_progress = (src) => {
* @function * @function
* @returns {void} * @returns {void}
*/ */
const clear_user_progress = () => USER_PROGRESS_STORE.set(DEFAULT_USER_PROGRESS); const clear_user_progress = () =>
USER_PROGRESS_STORE.set(DEFAULT_USER_PROGRESS);
/** /**
* User progress object * User progress object
-2
View File
@@ -3,6 +3,4 @@
* @module utility * @module utility
*/ */
export * from './announce';
export * from './seconds-to-timestamp'; export * from './seconds-to-timestamp';
export * from './use-url';
-59
View File
@@ -1,59 +0,0 @@
/*
Language: Svelte.js
Requires: xml.js, javascript.js, css.js
Author: Alexey Schebelev
Description: Components of Svelte Framework
Link: https://github.com/AlexxNB/highlightjs-svelte/blob/master/src/svelte.js
*/
import type { HLJSApi } from 'highlight.js';
export function hljsDefineSvelte(hljs: HLJSApi) {
return {
subLanguage: 'xml',
contains: [
hljs.COMMENT('<!--', '-->', { relevance: 10 }),
{
begin: /^(\s*)(<script(\s*context="module")?>)/gm,
end: /^(\s*)(<\/script>)/gm,
subLanguage: 'javascript',
excludeBegin: true,
excludeEnd: true,
contains: [
{
begin: /^(\s*)(\$:)/gm,
end: /(\s*)/gm,
className: 'keyword',
},
],
},
{
begin: /^(\s*)(<style.*>)/gm,
end: /^(\s*)(<\/style>)/gm,
subLanguage: 'css',
excludeBegin: true,
excludeEnd: true,
},
{
begin: /\{/gm,
end: /\}/gm,
subLanguage: 'javascript',
contains: [
{
// eslint-disable-next-line no-useless-escape
begin: /[\{]/,
// eslint-disable-next-line no-useless-escape
end: /[\}]/,
skip: true,
},
{
// eslint-disable-next-line no-useless-escape
begin: /([#:\/@])(if|else|each|await|then|catch|debug|html)/gm,
className: 'keyword',
relevance: 10,
},
],
},
],
};
}
-1
View File
@@ -1 +0,0 @@
export * from './use-markdown';
-103
View File
@@ -1,103 +0,0 @@
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeHighlight from 'rehype-highlight';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeSlug from 'rehype-slug';
// import rehypeAddClasses from 'rehype-add-classes';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkGithub from 'remark-github';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkToc from 'remark-toc';
import { unified } from 'unified';
import { hljsDefineSvelte } from './hljs-svelte';
export async function use_markdown(markdown: string) {
const pipeline = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkGithub, { repository: 'https://github.com/OllieJT/svelte-podcast.git' })
.use(remarkToc, { tight: true, ordered: true, maxDepth: 4 })
.use(remarkRehype)
.use(rehypeStringify)
.use(rehypeSanitize, {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
pre: [
...(defaultSchema.attributes?.pre || []),
[
'className',
'language-html',
'language-typescript',
'language-javascript',
'language-svelte',
'language-sh',
],
],
code: [
...(defaultSchema.attributes?.code || []),
[
'className',
'language-html',
'language-typescript',
'language-javascript',
'language-svelte',
'language-sh',
],
],
span: [
...(defaultSchema.attributes?.span || []),
[
'className',
'token',
'comment',
'prolog',
'cdata',
'punctuation',
'builtin',
'constant',
'boolean',
'number',
'important',
'atrule',
'property',
'keyword',
'doctype',
'operator',
'inserted',
'tag',
'class-name',
'symbol',
'attr-name',
'function',
'deleted',
'selector',
'attr-value',
'regex',
'char',
'string',
'entity',
'url',
'variable',
'bold',
'italic',
'namespace',
],
],
},
})
// .use(rehypeAddClasses, {})
.use(rehypeHighlight, { languages: { svelte: hljsDefineSvelte } })
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, { behavior: 'wrap' })
.use(rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] });
const output = await pipeline.process(markdown);
return output;
}
+178 -58
View File
@@ -1,67 +1,187 @@
<script lang="ts"> <script>
import { assets, base } from '$app/paths'; import Sidebar from '$src/layout/sidebar.svelte';
import { page } from '$app/stores'; import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte';
import { Github } from '@inqling/svelte-icons/simple-icons'; import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte';
import clsx from 'clsx'; import github from 'svelte-highlight/styles/github';
import 'highlight.js/styles/github-dark.css';
import { AudioContext } from 'svelte-podcast'; import { AudioContext } from 'svelte-podcast';
import { circIn, circOut } from 'svelte/easing';
import { fade } from 'svelte/transition';
import '../app.postcss'; import '../app.postcss';
const page_links = [ const page_links = Object.freeze([
{ label: 'Docs', href: '/' }, {
{ label: 'Demo', href: '/demo' }, label: 'About',
] as const; href: '/',
sections: [],
},
{
label: 'Setup',
href: '/setup',
sections: [
{ label: 'installation', anchor: 'installation' },
{ label: 'Loader', anchor: 'add_loader' },
],
},
{
label: 'API',
href: '/api',
sections: [
{ label: 'Episode Audio', anchor: 'episode_audio' },
{ label: 'Load Audio Source', anchor: 'load_audio_source' },
{ label: 'Episode Details', anchor: 'episode_details' },
{ label: 'Episode Progress', anchor: 'episode_progress' },
{ label: 'Seconds To Timestamp', anchor: 'seconds_to_timestamp' },
{ label: 'User Preferences', anchor: 'user_preferences' },
{ label: 'User Progress', anchor: 'user_progress' },
],
},
{
label: 'Examples',
href: '/examples',
sections: [],
},
]);
const resource_links = Object.freeze([
{
label: 'GitHub',
icon: GitHubIcon,
href: 'https://github.com/OllieJT/svelte-podcast',
},
{
label: 'NPM',
icon: NPMIcon,
href: 'https://www.npmjs.com/package/svelte-podcast',
},
]);
let is_menu_open = false;
const open_menu = () => (is_menu_open = true);
const close_menu = () => (is_menu_open = false);
</script> </script>
<header <svelte:head>
class="sticky top-0 z-50 border-b border-mono-100 bg-white text-lg leading-none lg:overflow-y-visible" {@html github}
id="navigation" </svelte:head>
>
<div class="mx-auto max-w-prose px-4 py-1 sm:px-6 sm:py-2 lg:px-8">
<div class="relative grid grid-cols-12 gap-1 text-base md:gap-3">
<div class="col-span-2 flex items-center justify-start leading-none">
<a href="{base}/#navigation" class="flex-shrink-0 py-1">
<img
class="block h-8 w-auto"
width={60}
height={32}
src="{assets}/logo.png"
alt="Svelte-Podcast"
/>
</a>
</div>
<div class="col-span-8 flex items-center justify-center leading-none sm:gap-1">
{#each page_links as link}
{@const is_homepage = link.href === '/' && $page.url.pathname === link.href}
{@const is_match = link.href !== '/' && $page.url.pathname.startsWith(link.href)}
<a
href={base + link.href}
class={clsx(
'flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium leading-none ring-inset focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 focus:ring-offset-2 sm:px-4 sm:text-base',
is_homepage || is_match
? 'bg-primary-200 text-primary-900'
: 'text-mono-500 hover:bg-mono-100 hover:text-mono-900',
)}
>
{link.label}
</a>
{/each}
</div>
<div class="col-span-2 flex items-center justify-end leading-none">
<a
href="https://github.com/OllieJT/svelte-podcast"
target="_blank"
rel="noreferrer"
class="flex-shrink-0 rounded-full bg-white p-1 text-mono-500 ring-2 ring-transparent transition-all ease-out hover:text-mono-900 hover:ring-mono-200 hover:ring-offset-2 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
<span class="sr-only">View Repository</span>
<Github class="h-6 w-6" aria-hidden="true" />
</a>
</div>
</div>
</div>
</header>
<AudioContext /> <AudioContext />
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
<div
class="pointer-events-none relative z-50 lg:hidden"
role="dialog"
aria-modal="true"
>
{#if is_menu_open}
<div
in:fade={{
duration: 300,
easing: circOut,
}}
out:fade={{
duration: 300,
easing: circIn,
}}
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"
>
<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>
</div>
<Sidebar
class="pb-2"
pages={page_links}
resources={resource_links}
/>
</div>
{/if}
</div>
</div>
<!-- Static sidebar for desktop -->
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<Sidebar
class="border-r border-mono-200"
pages={page_links}
resources={resource_links}
/>
</div>
<div
class="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden"
>
<button
on:click={open_menu}
type="button"
class="-m-2.5 p-2.5 text-mono-700 lg:hidden"
>
<span class="sr-only">Open sidebar</span>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
<div class="flex-1 text-sm font-semibold leading-6 text-mono-900">Menu</div>
<div class="flex items-center gap-4">
{#each resource_links as link}
<a
href={link.href}
target="_blank"
class="flex h-8 w-8 items-center justify-center rounded-full bg-mono-100 text-mono-600 hover:bg-mono-800 hover:text-white"
>
<span class="sr-only">{link.label}</span>
<svelte:component this={link.icon} class="h-6 w-6" />
</a>
{/each}
</div>
</div>
<main class="py-10 lg:pl-72">
<div class="px-4 sm:px-6 lg:px-8">
<slot /> <slot />
</div>
</main>
<footer />
-8
View File
@@ -1,8 +0,0 @@
import content from '$src/content/docs.md?raw';
import { use_markdown } from '$src/markdown';
import type { PageServerLoad } from './$types';
export const load = (async () => {
const post = await use_markdown(content);
return { html: post.value };
}) satisfies PageServerLoad;
+58 -28
View File
@@ -1,20 +1,19 @@
<script lang="ts"> <script>
import { base } from '$app/paths'; import { base } from '$app/paths';
import { Section } from '$src/content/components'; import { PlayerWidget } from '$src/components/example-player';
// import Docs from '$src/content/docs.md';
//import { PodcastPlayer } from '$src/components/example-player/_player';
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
import { episodes } from '$src/content/episodes'; import { episodes } from '$src/content/episodes';
import type { PageServerData } from './$types'; import { DocsPage } from '$src/layout/page';
export let data: PageServerData; /** @type { string | undefined} */
let audio_src = episodes.knomii.src;
let audio_src: string | undefined = episodes.knomii.src;
</script> </script>
<!-- <PodcastPlayer title="Example title" /> --> <DocsPage title="Docs" let:Section>
<header
<div class="relative isolate bg-white"> slot="header"
class="relative isolate bg-white"
aria-labelledby="hero"
>
<svg <svg
class="absolute inset-0 -z-10 h-full w-full stroke-mono-200 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]" class="absolute inset-0 -z-10 h-full w-full stroke-mono-200 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]"
aria-hidden="true" aria-hidden="true"
@@ -38,40 +37,45 @@
fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)" fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)"
/> />
</svg> </svg>
<main> <div>
<div class="relative py-24 sm:py-32 lg:pb-40"> <div class="relative py-24">
<div class="mx-auto max-w-7xl px-6 lg:px-8"> <div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center"> <div class="mx-auto max-w-2xl text-center">
<h1> <h1 id="hero">
<span class="block text-xl font-medium text-primary-600"> <span class="block text-xl font-medium text-primary-600">
svelte-podcast<span class="sr-only">: </span> svelte-podcast<span class="sr-only">: </span>
</span> </span>
<span class="text-4xl font-bold tracking-tight text-mono-900 sm:text-6xl"> <span
class="text-4xl font-bold tracking-tight text-mono-900 sm:text-6xl"
>
The fastest way to build a podcast site with Svelte. The fastest way to build a podcast site with Svelte.
</span> </span>
</h1> </h1>
<p class="mt-6 text-xl leading-8 text-mono-600"> <p class="mt-6 text-xl leading-8 text-mono-600">
A suite of tools and components to build your own podcast players, and work with A suite of tools and components to build your own podcast
RSS podcast data in SvelteKit. players, and work with RSS podcast data in SvelteKit.
<span class="mt-3 block text-base leading-none text-primary-800"> <span
class="mt-3 block text-base leading-none text-primary-800"
>
<span <span
class="inline-block rounded-full bg-primary-50 px-3 py-1.5 text-xs font-medium uppercase tracking-wider text-primary-600" class="inline-block rounded-full bg-primary-50 px-3 py-1.5 text-xs font-medium uppercase tracking-wider text-primary-600"
> >
Coming Soon<span class="sr-only">:</span> Coming Soon<span class="sr-only">:</span>
</span> </span>
<span class="tracking-wide">SSR utilities for consuming RSS podcast feeds</span <span class="tracking-wide"
>SSR utilities for consuming RSS podcast feeds</span
> >
</span> </span>
</p> </p>
<div class="mt-10 flex items-center justify-center gap-x-3"> <div class="mt-10 flex items-center justify-center gap-x-3">
<a <a
href="{base}/#get-started" href="{base}/setup"
class="rounded-md bg-primary-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600" class="rounded-md bg-primary-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
> >
Get started Get started
</a> </a>
<a <a
href="{base}/demo" href="{base}/api"
class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-mono-600 hover:bg-primary-50 hover:text-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600" class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-mono-600 hover:bg-primary-50 hover:text-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
> >
Examples <span aria-hidden="true"></span> Examples <span aria-hidden="true"></span>
@@ -80,7 +84,7 @@
</div> </div>
<div class="mt-16 flow-root sm:mt-24"> <div class="mt-16 flow-root sm:mt-24">
<div <div
class="-m-2 rounded-xl border border-mono-100 bg-white p-1 shadow-2xl shadow-mono-200 sm:p-2 lg:-m-4 lg:rounded-2xl lg:p-4" class="-m-2 grid grid-cols-1 grid-rows-1 rounded-xl border border-mono-100 bg-white p-1 shadow-2xl shadow-mono-200 sm:p-2 lg:-m-4 lg:rounded-2xl lg:p-4"
> >
<PlayerWidget <PlayerWidget
src={audio_src} src={audio_src}
@@ -97,11 +101,37 @@
</div> </div>
</div> </div>
</div> </div>
</main>
</div> </div>
</header>
<Section> <Section title="Introduction">
<div class="prose prose-lg prose-slate"> <p>
{@html data.html} Svelte-Podcast streamlines the creation of custom audio players and
</div> simplifies state management in Svelte apps.
</p>
<p>
<b>Build custom Audio Player UI</b>
<br />
Simplify the creation of custom audio players with a set of headless components
that keep out of your way and take care of core functionality.
</p>
<p>
<b>Easily manage Audio State</b>
<br />
Loading, controlling, and keeping track of multiple audio sources is a pain.
svelte-podcast abstracts this away and provides a simple interface to manage
audio state.
</p>
<p>
<b>Track user preferences</b>
<br />
Users expect a lot from media players. It should remember their preferences
like playback speed, and it should remember where they were in an episode
even after reloading the page. svelte-podcast takes care of this for you,
and provides you with access to extend this with your own database.
</p>
</Section> </Section>
</DocsPage>
+349
View File
@@ -0,0 +1,349 @@
<script>
import { DocsPage } from '$src/layout/page';
import { Highlight } from 'svelte-highlight';
import lang_ts from 'svelte-highlight/languages/typescript';
import {
load_audio_local,
load_audio_remote,
override_episode_state,
seconds_to_timestamp,
} from './code';
</script>
<DocsPage
title="API"
let:Section
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
rows={[
{
property: 'will_autoplay',
type: 'boolean',
description: 'Whether episodes will autoplay once loaded.',
},
{
property: 'is_paused',
type: 'boolean',
description: 'Whether the episode is paused.',
},
{
property: 'duration',
type: 'number',
description: 'The duration of the episode in seconds.',
},
{
property: 'src',
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:
'The details of the episode or null if there are none.',
},
]}
/>
<SectionArticle title="Load audio source">
<p>
All you need to load an episode is a URL to an audio file.
svelte-podcast uses a html audio element under the hood, so any
audio file compatible with the autio element is also compatible with
this package.
</p>
<h4>Using a remote file (URL)</h4>
<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>
<h4>Using a local file (relative path)</h4>
<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>
</SectionArticle>
</Section>
<Section title="episode_details">
<h3>Methods</h3>
<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
rows={[
{
property: 'current_time',
type: 'number',
description: 'The current time of the episode in seconds.',
},
{
property: 'timestamp',
type: 'string',
description:
"The current time of the episode in the format 'mm:ss'.",
},
{
property: 'has_ended',
type: 'boolean',
description: 'Whether the episode has ended or not.',
},
]}
/>
</Section>
<!-- seconds_to_timestamp -->
<Section title="seconds_to_timestamp">
<TableSchema
rows={[
{
property: 'seconds',
type: 'number',
description: 'The number of seconds to convert.',
},
{
property: 'force_hours',
type: 'boolean',
description:
"Whether to include hours in the timestamp, even if it's 0.",
},
]}
/>
<div class="not-prose codeblock">
<Highlight code={seconds_to_timestamp} language={lang_ts} />
</div>
</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>
+15
View File
@@ -0,0 +1,15 @@
import { default as load_audio_after_click_src } from './load-audio-after-click.svelte?raw';
import { default as load_audio_after_mount_src } from './load-audio-after-mount.svelte?raw';
import { default as load_audio_local_src } from './load-audio-local?raw';
import { default as load_audio_remote_src } from './load-audio-remote?raw';
import { default as override_episode_state_src } from './override-episode-state?raw';
import { default as seconds_to_timestamp_src } from './seconds-to-timestamp?raw';
const format_code = (/** @type {string} */ code) => code.replaceAll('\t', ' ');
export const load_audio_after_click = format_code(load_audio_after_click_src);
export const load_audio_after_mount = format_code(load_audio_after_mount_src);
export const load_audio_local = format_code(load_audio_local_src);
export const load_audio_remote = format_code(load_audio_remote_src);
export const override_episode_state = format_code(override_episode_state_src);
export const seconds_to_timestamp = format_code(seconds_to_timestamp_src);
+9
View File
@@ -0,0 +1,9 @@
import { episode_audio } from 'svelte-podcast';
episode_audio.load(
// path to file
'/episode-audio.mp3',
// custom metadata
{},
);
+9
View File
@@ -0,0 +1,9 @@
import { episode_audio } from 'svelte-podcast';
episode_audio.load(
// url
'https://media.transistor.fm/27a058c9/27b595e2.mp3',
// custom metadata
{},
);
@@ -0,0 +1,11 @@
// include this in your /src/app.d.ts file
declare module 'svelte-podcast' {
interface EpisodeDetails {
// define your own properties here
title: string;
artwork?: string;
}
}
export {};
@@ -0,0 +1,8 @@
import { seconds_to_timestamp } from 'svelte-podcast';
// Example
seconds_to_timestamp(5173); // 01:26:13
// force hours
seconds_to_timestamp(2700, true); // 00:45:00
seconds_to_timestamp(2700, false); // 45:00 (default)
-270
View File
@@ -1,270 +0,0 @@
<script lang="ts">
import PlayerStack from '$src/components/example-player/player-stack.svelte';
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
import PreviewComponent from '$src/components/preview-component.svelte';
import PreviewDataCode from '$src/components/preview-data-code.svelte';
import PreviewData from '$src/components/preview-data.svelte';
import { Section } from '$src/content/components';
import {
episode_audio,
episode_progress,
user_preferences,
user_progress,
} from 'svelte-podcast';
const sources = {
syntax: {
src: `/example-syntax.mp3`,
title: `Supper Club × Rich Harris, Author of Svelte`,
artwork: `https://ssl-static.libsyn.com/p/assets/b/3/c/d/b3cdf28da11ad39fe5bbc093207a2619/Syntax_-_499.jpg`,
},
knomii: {
src: `/example-knomii.mp3`,
title: `Empowerment starts with letting go of control`,
artwork: `https://ssl-static.libsyn.com/p/assets/f/a/8/d/fa8d56d5226884335f2e77a3093c12a1/ep-6.png`,
},
} as const;
// let current_time = 0;
// $: current_time = $episode_progress.current_time;
$: console.log('details :: ', $episode_audio?.details);
let player_widget_current_time = true;
$: player_widget = {
current_time: true,
playback_rate: true,
duration: true,
skip_back: 10,
skip_forward: 30,
};
$: player_stack = {
playback_rate: true,
timestamps: true,
skip_back: 10,
skip_forward: 30,
};
</script>
<Section>
<div class="flex flex-col items-stretch space-y-6">
<h1>This page is a work in progress</h1>
</div>
</Section>
<Section>
<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>
<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 include={player_widget} />
</PreviewComponent>
</Section>
<Section>
<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 class="max-w-sm" include={player_stack} />
</PreviewComponent>
</Section>
<Section>
<div class="prose prose-lg prose-slate">
<h2>Data</h2>
<p>
The following are the stores that are available to you based on the active episode being
played in the above examples.
</p>
<PreviewData name="episode_audio" data={episode_audio}>
<PreviewDataCode
code={`episode_audio.subscribe()`}
description="subscribes to the episode_audio data"
/>
<PreviewDataCode
code={`episode_audio.load(\n\tsrc: string,\n\tdetails: { [key: string]: string | number | boolean] }\n)`}
description="loads an episode into the player"
/>
<PreviewDataCode
code={`episode_audio.load(src: string, details: { [key: string]: string | number | boolean] })`}
description="loads an episode into the player"
/>
<PreviewDataCode
code={`episode_audio.unload()`}
description="unloads the episode from the player"
/>
<PreviewDataCode
code={`episode_audio.seek(seconds: number, from: 'from-start' | 'from-end' = 'from-start')`}
description="seeks to a specific part of the audio"
/>
<PreviewDataCode
code={`episode_audio.skip(seconds: number, type: 'forward' | 'backward' = 'forward')`}
description="seeks forward or backward relative to the current time"
/>
<PreviewDataCode
code={`episode_audio.play(action: "toggle" | "set" = "set")`}
description="plays the audio currently loaded in the player"
/>
<PreviewDataCode
code={`episode_audio.pause(action: "toggle" | "set" = "set")`}
description="pauses the audio currently loaded in the player"
/>
<PreviewDataCode
code={`episode_audio.mute(action: "toggle" | "set" = "set")`}
description="mutes the audio player"
/>
<PreviewDataCode
code={`episode_audio.unmute(action: "toggle" | "set" = "set")`}
description="unmutes the audio player"
/>
</PreviewData>
<PreviewData name="episode_progress" data={episode_progress}>
<PreviewDataCode
code={`episode_progress.subscribe()`}
description="subscribes to the episode_progress data"
/>
</PreviewData>
<PreviewData name="user_preferences" data={user_preferences}>
<PreviewDataCode
code={`user_preferences.subscribe()`}
description="subscribes to the user_preferences data"
/>
<PreviewDataCode
code={`user_preferences.clear()`}
description="clears all user preferences and resets them to default values"
/>
<PreviewDataCode
code={`user_preferences.set_playback_rate(value: number)`}
description="sets the users prefered playback_rate (speed)"
/>
<PreviewDataCode
code={`user_preferences.set_volume(value: number)`}
description="sets the users prefered volume"
/>
</PreviewData>
<PreviewData name="user_progress" data={user_progress}>
<PreviewDataCode
code={`user_progress.subscribe()`}
description="subscribes to the user_progress data"
/>
<button type="button" on:click={user_progress.clear}>user_progress.clear()</button>
<PreviewDataCode
code={`user_progress.clear()`}
description="clears all user progress for all episodes"
/>
<button type="button" on:click={user_progress.clear}>user_progress.clear()</button>
<PreviewDataCode
code={`user_progress.get(src:string)`}
description="gets the users progress (seconds) for a specific episode"
/>
<button type="button" on:click={user_progress.save}>user_progress.save()</button>
<PreviewDataCode
code={`user_progress.save()`}
description="saves all user progress for all episodes they have interacted with in the current session"
/>
</PreviewData>
</div>
</Section>
<h1>Demo</h1>
<button type="button" on:click={user_preferences.clear}>Clear all preferences</button>
<hr />
<h5>Load Audio</h5>
<button type="button" on:click={() => episode_audio.load(sources['syntax'].src, sources['syntax'])}>
Syntax
</button>
<button type="button" on:click={() => episode_audio.load(sources['knomii'].src, sources['knomii'])}
>Knomii</button
>
<button type="button" on:click={() => episode_audio.unload()}>None</button>
<hr />
<h5>Custom audio controls</h5>
<h6>Play / Pause Actions</h6>
<button type="button" on:click={() => episode_audio.play()}>Play</button>
<button type="button" on:click={() => episode_audio.pause()}>Pause</button>
<button type="button" on:click={() => episode_audio.pause('toggle')}>Toggle</button>
<hr />
<h6>Audio Actions</h6>
<button type="button" on:click={() => episode_audio.mute()}>Mute</button>
<button type="button" on:click={() => episode_audio.unmute()}>Unmute</button>
<button type="button" on:click={() => episode_audio.mute('toggle')}>Toggle</button>
<hr />
<h6>Seeking</h6>
<button type="button" on:click={() => episode_audio.seek(30)}>Go to 30s from start </button>
<button type="button" on:click={() => episode_audio.seek(30, 'from-end')}>Go to 30s from end</button
>
<button type="button" on:click={() => episode_audio.skip(10, 'forward')}>Skip 10s</button>
<button type="button" on:click={() => episode_audio.skip(10, 'backward')}>Rewind 10s</button>
<hr />
<h6>Playback Rate</h6>
{#each [0.5, 1, 2, 3] as rate}
<button type="button" on:click={() => user_preferences.set_playback_rate(rate)}>
{rate}x
</button>
{/each}
<!-- <PodcastPlayer
artwork={$episode_audio?.details?.artwork}
title={$episode_audio?.details?.title || 'Podcast Name'}
/> -->
+126
View File
@@ -0,0 +1,126 @@
<script>
import PlayerStack from '$src/components/example-player/player-stack.svelte';
import PlayerWidget from '$src/components/example-player/player-widget.svelte';
import { episodes } from '$src/content/episodes';
import { DocsPage } from '$src/layout/page';
import PreviewComponent from '$src/layout/preview-component.svelte';
import { episode_audio } from 'svelte-podcast';
$: console.log('details :: ', $episode_audio?.details);
$: player_widget = {
current_time: true,
playback_rate: true,
duration: true,
skip_back: 10,
skip_forward: 30,
};
$: player_stack = {
playback_rate: true,
timestamps: true,
skip_back: 10,
skip_forward: 30,
};
/** @type { string | undefined} */
let audio_src = episodes.knomii.src;
</script>
<DocsPage
title="Examples"
let:Section
let:SectionArticle
let:TableModule
let:TableSchema
>
<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>
<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>
</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>
+53
View File
@@ -0,0 +1,53 @@
<script>
import { DocsPage } from '$src/layout/page';
import { add_audio_loader, install } from './code';
import { Highlight, HighlightSvelte } from 'svelte-highlight';
import lang_shell from 'svelte-highlight/languages/shell';
</script>
<DocsPage title="Setup" let:Section>
<Section title="Installation">
<p>
Install the latest version of svelte-podcast with your preferred
package manager.
</p>
<div class="codeblock not-prose flex flex-col items-stretch gap-2 py-1">
<Highlight language={lang_shell} code={install} />
</div>
</Section>
<Section title="Add Loader">
<p class="text-primary-600">
<b class="font-medium">Important</b>: The AudioLoader component does
<em>not</em>
render any UI. However it <em>is required</em> to use svelte-podcast.
</p>
<p>
This component is responsible for loading the audio sources and
persisting the audio state between page loads. This means that your
audio will continue playing even if the user refreshes the page, or you
use different UI for the media player.
</p>
<p>
Add the AudioLoader component to your app. It should be as close to the
root of your app as possible to ensure the audio state behaves as
expected in nested routes.
</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">
<HighlightSvelte code={add_audio_loader} />
</div>
</Section>
</DocsPage>
@@ -4,6 +4,4 @@
<AudioContext /> <AudioContext />
<!-- your layout -->
<slot /> <slot />
+6
View File
@@ -0,0 +1,6 @@
import { format_code } from '$src/layout/helper';
import { default as add_audio_loader_src } from './add-audio-loader.svelte?raw';
import { default as install_src } from './install.sh?raw';
export const add_audio_loader = format_code(add_audio_loader_src);
export const install = format_code(install_src);
+5
View File
@@ -0,0 +1,5 @@
npm install svelte-podcast@latest
yarn add svelte-podcast@latest
pnpm add svelte-podcast@latest
View File
+7
View File
@@ -0,0 +1,7 @@
/* Maintainer */
Name: Ollie Taylor
Website: https://olliejt.com
/* Contributor */
Name: Ollie Taylor
Website: https://olliejt.com
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+15 -2
View File
@@ -1,7 +1,11 @@
const colors = require('tailwindcss/colors'); const colors = require('tailwindcss/colors');
const plugin = require('tailwindcss/plugin');
const config = { const config = {
content: ['./src/**/*.{html,js,md,mdx,svx,svelte,ts}', './node_modules/svhighlight/**/*.svelte'], content: [
'./src/**/*.{html,js,md,mdx,svx,svelte,ts}',
'./node_modules/svhighlight/**/*.svelte',
],
theme: { theme: {
extend: { extend: {
@@ -16,7 +20,16 @@ const config = {
}, },
}, },
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')], plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
plugin(function ({ addVariant }) {
addVariant('hocus', ['&:hover', '&:focus', '&:focus-visible']);
addVariant('focus', ['&:focus', '&:focus-visible']);
addVariant('active', ['&.active']);
}),
],
}; };
module.exports = config; module.exports = config;
+24 -1055
View File
File diff suppressed because it is too large Load Diff