Update Docs Site (#44)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte-podcast': minor
|
||||
---
|
||||
|
||||
switch to jsdoc from ts
|
||||
+6
-1
@@ -32,7 +32,12 @@ module.exports = {
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
|
||||
+2
-14
@@ -60,33 +60,21 @@
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"postcss": "^8.4.25",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"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-check": "^3.4.5",
|
||||
"svelte-highlight": "^7.3.0",
|
||||
"svelte-meta-tags": "^2.8.0",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svhighlight": "^0.7.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"unified": "^10.1.2",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"svelte": "./dist/index.js",
|
||||
|
||||
Vendored
+4
@@ -15,4 +15,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.md' {
|
||||
export default ComponentType<SvelteComponentTyped>;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
+10
-3
@@ -1,12 +1,19 @@
|
||||
<!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>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.png" />
|
||||
<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%
|
||||
</head>
|
||||
<body class="scroll-smooth hover:scroll-auto">
|
||||
<body class="h-full scroll-smooth hover:scroll-auto">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+101
-169
@@ -16,36 +16,14 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.prose code {
|
||||
@apply inline rounded bg-primary-100 px-2 py-1 font-mono leading-none text-primary-800;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.prose code::before,
|
||||
.prose code::after {
|
||||
content: '';
|
||||
}
|
||||
h1.anchor,
|
||||
h2.anchor,
|
||||
h3.anchor,
|
||||
h4.anchor,
|
||||
h5.anchor,
|
||||
h6.anchor {
|
||||
@apply m-0 text-3xl font-semibold leading-10 text-mono-900;
|
||||
}
|
||||
.richtext h1 a,
|
||||
.richtext h2 a,
|
||||
.richtext h3 a,
|
||||
.richtext h4 a,
|
||||
.richtext h5 a,
|
||||
.richtext h6 a {
|
||||
@apply relative underline decoration-transparent decoration-solid underline-offset-2;
|
||||
|
||||
.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 {
|
||||
@apply absolute -left-[1.25ch] inline-block font-light leading-normal text-mono-400;
|
||||
content: '#';
|
||||
@@ -58,168 +36,122 @@
|
||||
}
|
||||
}
|
||||
|
||||
.prose h3 a {
|
||||
@apply text-primary-600;
|
||||
.richtext code.hljs {
|
||||
@apply p-0 text-sm;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.prose #table-of-contents + ol {
|
||||
list-style: decimal-leading-zero;
|
||||
}
|
||||
.prose #table-of-contents + ol,
|
||||
.prose #table-of-contents + ul {
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
@apply my-0.5;
|
||||
.richtext .codeblock code.hljs {
|
||||
@apply overflow-hidden rounded-xl border border-mono-200 p-4 font-mono text-base leading-normal shadow-2xl shadow-mono-200;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: lower-alpha;
|
||||
ol {
|
||||
list-style: disc;
|
||||
ol {
|
||||
list-style: circle;
|
||||
}
|
||||
}
|
||||
}
|
||||
.richtext {
|
||||
@apply prose prose-lg prose-slate max-w-none prose-h5:mb-1 prose-h5:mt-10 prose-h5:text-sm prose-h5:font-medium prose-h5:leading-normal prose-h5:text-primary-800 prose-code:before:content-none prose-code:after:content-none;
|
||||
}
|
||||
|
||||
.prose #features + table td:first-child > * {
|
||||
@apply sm:block sm:w-max;
|
||||
}
|
||||
.richtext td,
|
||||
.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
|
||||
* https://github.com/Jaredk3nt/laserwave
|
||||
*
|
||||
* Ported for PrismJS by Simon Jespersen [https://github.com/simjes]
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/
|
||||
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
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 {
|
||||
color: #24292e;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
code[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] ::-moz-selection,
|
||||
pre[class*='language-']::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection {
|
||||
background: #eb64b927;
|
||||
color: inherit;
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
code[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection,
|
||||
pre[class*='language-']::selection,
|
||||
pre[class*='language-'] ::selection {
|
||||
background: #eb64b927;
|
||||
color: inherit;
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
/* Properties specific to code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em; /* this is standard */
|
||||
margin: 0.5em 0; /* this is the default */
|
||||
overflow: auto; /* this is standard */
|
||||
border-radius: 0.5em;
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id,
|
||||
.hljs-variable {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
/* Properties specific to inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.2em 0.3em;
|
||||
border-radius: 0.5rem;
|
||||
white-space: normal; /* this is standard */
|
||||
.hljs-meta .hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-string {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.cdata {
|
||||
color: #91889b;
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
color: #e36209;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #7b6995;
|
||||
.hljs-code,
|
||||
.hljs-comment,
|
||||
.hljs-formula {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.token.builtin,
|
||||
.token.constant,
|
||||
.token.boolean {
|
||||
color: #ffe261;
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-selector-tag {
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #b381c5;
|
||||
.hljs-subst {
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.property,
|
||||
.token.keyword {
|
||||
color: #40b4c4;
|
||||
.hljs-section {
|
||||
color: #005cc5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.token.doctype,
|
||||
.token.operator,
|
||||
.token.inserted,
|
||||
.token.tag,
|
||||
.token.class-name,
|
||||
.token.symbol {
|
||||
color: #74dfc4;
|
||||
.hljs-bullet {
|
||||
color: #735c0f;
|
||||
}
|
||||
|
||||
.token.attr-name,
|
||||
.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 {
|
||||
.hljs-emphasis {
|
||||
color: #24292e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
.hljs-strong {
|
||||
color: #24292e;
|
||||
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 PlayerWidget } from './player-widget.svelte';
|
||||
@@ -1,29 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Pause, Play, SpeakerWave } from '@inqling/svelte-icons/heroicon-20-solid';
|
||||
import { clsx } from 'clsx';
|
||||
import { AudioPlayer, episode_progress, user_preferences } from 'svelte-podcast';
|
||||
import type { EpisodeDetails } from 'svelte-podcast/audio/stores';
|
||||
<script>
|
||||
import {
|
||||
Pause,
|
||||
Play,
|
||||
SpeakerWave,
|
||||
} from '@inqling/svelte-icons/heroicon-20-solid';
|
||||
import {
|
||||
AudioPlayer,
|
||||
episode_progress,
|
||||
user_preferences,
|
||||
} from 'svelte-podcast';
|
||||
import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
|
||||
|
||||
export let src: string | undefined;
|
||||
export let metadata: EpisodeDetails = {};
|
||||
/** @type {string | undefined} */
|
||||
export let src;
|
||||
|
||||
export let hide_timestamps: boolean = false;
|
||||
export let hide_playback_rate: boolean = false;
|
||||
/** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
|
||||
export let metadata = {};
|
||||
|
||||
export let skip_back: number = 30;
|
||||
export let skip_forward: number = 10;
|
||||
export let hide_timestamps = false;
|
||||
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>
|
||||
|
||||
<AudioPlayer {src} {metadata} let:Player let:action let:episode>
|
||||
<div
|
||||
class={clsx('svpod--container svpod--reset', ClassName)}
|
||||
{...$$restProps}
|
||||
class="svpod--container svpod--reset"
|
||||
data-loaded={episode.is_loaded ? 'true' : 'false'}
|
||||
{...rest}
|
||||
>
|
||||
<div class="svpod--container--row svpod--row--controls">
|
||||
<button
|
||||
@@ -129,7 +136,9 @@
|
||||
--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--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;
|
||||
@@ -299,7 +308,11 @@
|
||||
height: 100%;
|
||||
content: '';
|
||||
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 {
|
||||
@@ -311,10 +324,15 @@
|
||||
|
||||
.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);
|
||||
--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)
|
||||
var(--svpod--timeline-thumb--shape--height) -
|
||||
var(--thumb-border-offset)
|
||||
);
|
||||
|
||||
--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 { clsx } from 'clsx';
|
||||
import { AudioPlayer, user_preferences } from 'svelte-podcast';
|
||||
import type { EpisodeDetails } from 'svelte-podcast/audio/stores';
|
||||
import { A11yIcon, Skip, Spinner, Timestamp } from './utility';
|
||||
|
||||
export let src: string | undefined;
|
||||
export let metadata: EpisodeDetails = {};
|
||||
/** @type {string | undefined} */
|
||||
export let src;
|
||||
|
||||
export let hide_duration: boolean = false;
|
||||
export let hide_current_time: boolean = false;
|
||||
export let hide_playback_rate: boolean = false;
|
||||
export let hide_skip_back: boolean = false;
|
||||
export let hide_skip_forward: boolean = false;
|
||||
/** @type {import('svelte-podcast/audio/stores').EpisodeDetails} */
|
||||
export let metadata = {};
|
||||
|
||||
export let skip_back: number = 30;
|
||||
export let skip_forward: number = 10;
|
||||
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 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>
|
||||
|
||||
<AudioPlayer {src} {metadata} let:Player let:action let:episode>
|
||||
<div
|
||||
class={clsx('svpod--container svpod--reset', ClassName)}
|
||||
{...$$restProps}
|
||||
class="svpod--container svpod--reset"
|
||||
data-loaded={episode.is_loaded ? 'true' : 'false'}
|
||||
{...rest}
|
||||
>
|
||||
{#if !hide_skip_back}
|
||||
<button
|
||||
@@ -68,7 +67,10 @@
|
||||
|
||||
{#if !hide_current_time}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
@@ -118,7 +120,9 @@
|
||||
--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--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;
|
||||
@@ -226,10 +230,15 @@
|
||||
|
||||
.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);
|
||||
--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)
|
||||
var(--svpod--timeline-thumb--shape--height) -
|
||||
var(--thumb-border-offset)
|
||||
);
|
||||
|
||||
--svpod--timeline-track--bg: var(--svpod--surface--base);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteIcon } from '@inqling/svelte-icons';
|
||||
<script>
|
||||
/** @type {import('@inqling/svelte-icons').SvelteIcon} */
|
||||
export let icon;
|
||||
|
||||
export let icon: SvelteIcon;
|
||||
export let label: string;
|
||||
/** @type {string} */
|
||||
export let label;
|
||||
</script>
|
||||
|
||||
<svelte:component this={icon} />
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
/** @type {number} [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>
|
||||
|
||||
<div class="svpod--skip-container" style="font-size:{size}px">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
/** @type {number} */
|
||||
export let size = 24;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { seconds_to_timestamp } from 'svelte-podcast';
|
||||
|
||||
export let value: number;
|
||||
/** @type {number} */
|
||||
export let value;
|
||||
|
||||
/** @type {boolean} */
|
||||
export let force_hours = false;
|
||||
|
||||
$: timestamp = seconds_to_timestamp(value, force_hours);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 |
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
export const episodes = {
|
||||
/**
|
||||
* Audio context module.
|
||||
* @module episodes
|
||||
*/
|
||||
|
||||
export const episodes = Object.freeze({
|
||||
syntax: {
|
||||
src: `${assets}/example-syntax.mp3`,
|
||||
title: `Supper Club × Rich Harris, Author of Svelte`,
|
||||
@@ -11,4 +16,4 @@ export const episodes = {
|
||||
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;
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
Vendored
-8
@@ -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>;
|
||||
}
|
||||
@@ -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', ' ');
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DocsPage } from './component.svelte';
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
export let name: string;
|
||||
<script>
|
||||
/** @type {string} */
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-stretch space-y-6">
|
||||
@@ -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>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { announce } from '../internal';
|
||||
import { user_preferences } from '../user';
|
||||
import { announce } from '../utility';
|
||||
import { audio_element } from './stores/audio-element';
|
||||
|
||||
$: $audio_element;
|
||||
@@ -24,9 +24,10 @@
|
||||
<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--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);
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
$: is_playing = $episode_audio?.is_paused === false;
|
||||
$: current_time = $episode_progress?.current_time || 0;
|
||||
$: duration = $episode_audio?.duration || 0;
|
||||
$: timestamp_hours = Boolean($episode_audio?.duration && $episode_audio.duration >= 3600);
|
||||
$: timestamp_hours = Boolean(
|
||||
$episode_audio?.duration && $episode_audio.duration >= 3600,
|
||||
);
|
||||
$: timestamp = duration && seconds_to_timestamp(duration);
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { episode_audio, episode_progress } from '../audio';
|
||||
import { announce } from '../utility';
|
||||
import { announce } from '../internal';
|
||||
|
||||
export let step = 10;
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ export const audio_element = readable(undefined, (set) => {
|
||||
|
||||
if (!existing_element) document.body.appendChild(el);
|
||||
|
||||
/*
|
||||
BUG: This is a hack to get around poor types.
|
||||
The input and output types are correct, so this is safe for now.
|
||||
*/
|
||||
// @ts-expect-error - I'm not sure how to fix this using JSDOC
|
||||
set(el);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clamp from 'just-clamp';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { user_preferences, user_progress } from '../../user';
|
||||
import { announce } from '../../utility';
|
||||
import { announce } from '../../internal';
|
||||
import { audio_element } from './audio-element';
|
||||
import { episode_details } from './episode-details';
|
||||
|
||||
@@ -29,7 +29,9 @@ const default_episode_state = {
|
||||
* Episode state store
|
||||
* @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);
|
||||
|
||||
function set_value() {
|
||||
@@ -54,7 +56,8 @@ const episode_state = derived([audio_element, episode_details], ([$audio, $detai
|
||||
$audio.removeEventListener('pause', set_value);
|
||||
$audio.removeEventListener('playing', set_value);
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef {'toggle' | 'set'} HANDLE_TYPE
|
||||
@@ -68,7 +71,8 @@ const episode_state = derived([audio_element, episode_details], ([$audio, $detai
|
||||
*/
|
||||
const use_element = (action) => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Utility functions module.
|
||||
* @module internal
|
||||
*/
|
||||
|
||||
export * from './announce';
|
||||
export * from './use-url';
|
||||
@@ -17,7 +17,10 @@ const DEFAULT_USER_PREFERENCES = { playback_rate: 1, volume: 1 };
|
||||
* The user preferences store
|
||||
* @type {import('svelte/store').Writable<UserPreferences>}
|
||||
*/
|
||||
const USER_PREFERENCES_STORE = persisted('USER_PREFERENCE', DEFAULT_USER_PREFERENCES);
|
||||
const USER_PREFERENCES_STORE = persisted(
|
||||
'USER_PREFERENCE',
|
||||
DEFAULT_USER_PREFERENCES,
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 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,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { persisted } from 'svelte-local-storage-store';
|
||||
import { get } from 'svelte/store';
|
||||
import { episode_audio, episode_progress } from '../audio';
|
||||
import { announce, use_url } from '../utility';
|
||||
import { use_url, announce } from '../internal';
|
||||
|
||||
/**
|
||||
* User progress object type.
|
||||
@@ -34,7 +34,10 @@ const save_user_progress = () => {
|
||||
|
||||
const pathname = use_url(audio.src).pathname;
|
||||
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
|
||||
* @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
|
||||
|
||||
@@ -3,6 +3,4 @@
|
||||
* @module utility
|
||||
*/
|
||||
|
||||
export * from './announce';
|
||||
export * from './seconds-to-timestamp';
|
||||
export * from './use-url';
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export * from './use-markdown';
|
||||
@@ -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
@@ -1,67 +1,187 @@
|
||||
<script lang="ts">
|
||||
import { assets, base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Github } from '@inqling/svelte-icons/simple-icons';
|
||||
import clsx from 'clsx';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
<script>
|
||||
import Sidebar from '$src/layout/sidebar.svelte';
|
||||
import GitHubIcon from '@inqling/svelte-icons/simple-icons/github.svelte';
|
||||
import NPMIcon from '@inqling/svelte-icons/simple-icons/npm.svelte';
|
||||
import github from 'svelte-highlight/styles/github';
|
||||
import { AudioContext } from 'svelte-podcast';
|
||||
import { circIn, circOut } from 'svelte/easing';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.postcss';
|
||||
|
||||
const page_links = [
|
||||
{ label: 'Docs', href: '/' },
|
||||
{ label: 'Demo', href: '/demo' },
|
||||
] as const;
|
||||
const page_links = Object.freeze([
|
||||
{
|
||||
label: 'About',
|
||||
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>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-mono-100 bg-white text-lg leading-none lg:overflow-y-visible"
|
||||
id="navigation"
|
||||
>
|
||||
<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>
|
||||
<svelte:head>
|
||||
{@html github}
|
||||
</svelte:head>
|
||||
|
||||
<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 />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer />
|
||||
|
||||
@@ -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
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { Section } from '$src/content/components';
|
||||
// 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 { PlayerWidget } from '$src/components/example-player';
|
||||
import { episodes } from '$src/content/episodes';
|
||||
import type { PageServerData } from './$types';
|
||||
import { DocsPage } from '$src/layout/page';
|
||||
|
||||
export let data: PageServerData;
|
||||
|
||||
let audio_src: string | undefined = episodes.knomii.src;
|
||||
/** @type { string | undefined} */
|
||||
let audio_src = episodes.knomii.src;
|
||||
</script>
|
||||
|
||||
<!-- <PodcastPlayer title="Example title" /> -->
|
||||
|
||||
<div class="relative isolate bg-white">
|
||||
<DocsPage title="Docs" let:Section>
|
||||
<header
|
||||
slot="header"
|
||||
class="relative isolate bg-white"
|
||||
aria-labelledby="hero"
|
||||
>
|
||||
<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)]"
|
||||
aria-hidden="true"
|
||||
@@ -38,40 +37,45 @@
|
||||
fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)"
|
||||
/>
|
||||
</svg>
|
||||
<main>
|
||||
<div class="relative py-24 sm:py-32 lg:pb-40">
|
||||
<div>
|
||||
<div class="relative py-24">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center">
|
||||
<h1>
|
||||
<h1 id="hero">
|
||||
<span class="block text-xl font-medium text-primary-600">
|
||||
svelte-podcast<span class="sr-only">: </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.
|
||||
</span>
|
||||
</h1>
|
||||
<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
|
||||
RSS podcast data in SvelteKit.
|
||||
<span class="mt-3 block text-base leading-none text-primary-800">
|
||||
A suite of tools and components to build your own podcast
|
||||
players, and work with RSS podcast data in SvelteKit.
|
||||
<span
|
||||
class="mt-3 block text-base leading-none text-primary-800"
|
||||
>
|
||||
<span
|
||||
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>
|
||||
</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>
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-3">
|
||||
<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"
|
||||
>
|
||||
Get started
|
||||
</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"
|
||||
>
|
||||
Examples <span aria-hidden="true">→</span>
|
||||
@@ -80,7 +84,7 @@
|
||||
</div>
|
||||
<div class="mt-16 flow-root sm:mt-24">
|
||||
<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
|
||||
src={audio_src}
|
||||
@@ -97,11 +101,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Section>
|
||||
<div class="prose prose-lg prose-slate">
|
||||
{@html data.html}
|
||||
</div>
|
||||
<Section title="Introduction">
|
||||
<p>
|
||||
Svelte-Podcast streamlines the creation of custom audio players and
|
||||
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>
|
||||
</DocsPage>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { episode_audio } from 'svelte-podcast';
|
||||
|
||||
episode_audio.load(
|
||||
// path to file
|
||||
'/episode-audio.mp3',
|
||||
|
||||
// custom metadata
|
||||
{},
|
||||
);
|
||||
@@ -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)
|
||||
@@ -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'}
|
||||
/> -->
|
||||
@@ -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>
|
||||
@@ -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 />
|
||||
|
||||
<!-- your layout -->
|
||||
|
||||
<slot />
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
npm install svelte-podcast@latest
|
||||
|
||||
yarn add svelte-podcast@latest
|
||||
|
||||
pnpm add svelte-podcast@latest
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Maintainer */
|
||||
Name: Ollie Taylor
|
||||
Website: https://olliejt.com
|
||||
|
||||
/* Contributor */
|
||||
Name: Ollie Taylor
|
||||
Website: https://olliejt.com
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
+15
-2
@@ -1,7 +1,11 @@
|
||||
const colors = require('tailwindcss/colors');
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
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: {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user