more controls, metadata

This commit is contained in:
wryk 2020-01-10 03:02:46 +01:00
parent 3783f0fa3a
commit 7e4038386e
12 changed files with 461 additions and 251 deletions

177
package-lock.json generated
View File

@ -847,11 +847,19 @@
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz",
"integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"@babel/runtime-corejs2": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.7.tgz",
"integrity": "sha512-P91T3dFYQL7aj44PxOMIAbo66Ag3NbmXG9fseSYaXxapp3K9XTct5HU9IpTOm2D0AoktKusgqzN5YcSxZXEKBQ==",
"requires": {
"core-js": "^2.6.5",
"regenerator-runtime": "^0.13.2"
}
},
"@babel/runtime-corejs3": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.7.tgz",
@ -1014,6 +1022,14 @@
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"ajv": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
@ -1081,11 +1097,15 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"argv": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
"integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas="
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -1272,8 +1292,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base": {
"version": "0.11.2",
@ -1376,7 +1395,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1761,6 +1779,18 @@
"q": "^1.1.2"
}
},
"codecov": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.6.1.tgz",
"integrity": "sha512-IUJB6WG47nWK7o50etF8jBadxdMw7DmoQg05yIljstXFBGB6clOZsIj6iD4P82T2YaIU3qq+FFu8K9pxgkCJDQ==",
"requires": {
"argv": "^0.0.2",
"ignore-walk": "^3.0.1",
"js-yaml": "^3.13.1",
"teeny-request": "^3.11.3",
"urlgrey": "^0.4.4"
}
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -1836,8 +1866,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.2",
@ -1881,8 +1910,7 @@
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
"dev": true
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
},
"core-js-compat": {
"version": "3.6.2",
@ -2662,6 +2690,19 @@
"is-symbol": "^1.0.2"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -3949,6 +3990,25 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"https-proxy-agent": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
}
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -3970,6 +4030,14 @@
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
"dev": true
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@ -4286,6 +4354,29 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"iter-tools": {
"version": "7.0.0-rc.0",
"resolved": "https://registry.npmjs.org/iter-tools/-/iter-tools-7.0.0-rc.0.tgz",
"integrity": "sha512-u5pdY3kERasb9eUiAuVIWoYSpUnKKv7xXol+KpwIT+U+/LUDT+F1xTPdSNLHcNxIxAcjIWG+Adu6jtkgCOKd4Q==",
"requires": {
"@babel/runtime": "^7.4.3",
"@babel/runtime-corejs2": "^7.4.3",
"codecov": "^3.6.1",
"decamelize": "^3.2.0",
"little-ds-toolkit": "^1.1.0",
"typescript-tuple": "^2.1.0"
},
"dependencies": {
"decamelize": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz",
"integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==",
"requires": {
"xregexp": "^4.2.4"
}
}
}
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@ -4302,7 +4393,6 @@
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -4311,8 +4401,7 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
}
}
},
@ -4453,6 +4542,11 @@
"type-check": "~0.3.2"
}
},
"little-ds-toolkit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/little-ds-toolkit/-/little-ds-toolkit-1.1.1.tgz",
"integrity": "sha512-Zl5flhnd5W6nhRCyoL1bNlU8M5CWFp6SItMmK4pj39LKgzQD7LGg591OJ0jwDKat7mjHvJVkOyJT+BXOQH4iXw=="
},
"load-script2": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/load-script2/-/load-script2-2.0.4.tgz",
@ -4654,7 +4748,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -4706,8 +4799,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nan": {
"version": "2.14.0",
@ -4747,6 +4839,11 @@
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==",
"dev": true
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"node-forge": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
@ -6514,8 +6611,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.16.1",
@ -6780,6 +6876,16 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"teeny-request": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
"integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
"requires": {
"https-proxy-agent": "^2.2.1",
"node-fetch": "^2.2.0",
"uuid": "^3.3.2"
}
},
"terser": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz",
@ -6942,6 +7048,27 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript-compare": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
"integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
"requires": {
"typescript-logic": "^0.0.0"
}
},
"typescript-logic": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
"integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q=="
},
"typescript-tuple": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
"integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
"requires": {
"typescript-compare": "^0.0.2"
}
},
"uncss": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/uncss/-/uncss-0.17.2.tgz",
@ -7140,6 +7267,11 @@
"tlds": "^1.203.0"
}
},
"urlgrey": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz",
"integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8="
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -7182,8 +7314,7 @@
"uuid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"dev": true
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
},
"v8-compile-cache": {
"version": "2.1.0",
@ -7357,6 +7488,14 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"xregexp": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.2.4.tgz",
"integrity": "sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA==",
"requires": {
"@babel/runtime-corejs2": "^7.2.0"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -21,6 +21,7 @@
"@babel/runtime-corejs3": "^7.7.7",
"date-fns": "^2.9.0",
"get-urls": "^9.2.0",
"iter-tools": "^7.0.0-rc.0",
"yt-player": "^3.4.3"
},
"browserslist": [

View File

@ -1,129 +0,0 @@
<main class="app">
<header class="header">
<h1>
Eldritch Radio
</h1>
</header>
<section class="player">
{#if $selectedEntry}
Playing <a href={$selectedEntry.url}>{$selectedEntry.id}</a>
<YoutubeViewer bind:videoId={$selectedEntry.id} bind:playing={$playing}></YoutubeViewer>
{:else}
Loading ...
{/if}
<div>
<button>⏮️</button>
<button on:click={() => $playing = !$playing}>{#if $playing}⏸️{:else}▶️{/if}</button>
<button on:click={() => selectedEntry.next()}>⏭️</button>
</div>
<div>
<button>🔇 / 🔊</button>
<input type="range" min="0" max="100" value="80">
</div>
<div>
<button></button>
<button>🔁</button>
</div>
</section>
<section class="queue">
<div>
{#if $entries}
<ul>
{#each $entries as entry}
<li class="entry" class:active={entry === $selectedEntry} on:click={() => $selectedEntry = entry}>
<div>{entry.url}</div>
<b>{entry.status.account.acct}</b>
<small>{entry.tags}</small>
</li>
{/each}
</ul>
{:else}
Your queue
{/if}
</div>
{#if $loading}
LOADING ...
{:else}
<button on:click={() => entries.load(3)}>LOAD MOAR</button>
{/if}
<header>
<a href="https://{$domain}/">{$domain}</a> - {@html $hashtags.map(hashtag => `<a href="https://${$domain}/tags/${hashtag}">#${hashtag}</a>`)}
</header>
</section>
</main>
<script>
import { onMount } from 'svelte'
import YoutubeViewer from './YoutubeViewer.svelte'
import { domain, hashtags, playing, loading, entry as selectedEntry, entries } from './store.js'
onMount(() => {
const unsub = entries.subscribe(async (xs) => {
if (xs.length) {
const [firstEntry] = xs
selectedEntry.set(firstEntry)
unsub()
}
})
entries.load(7)
})
</script>
<style>
.app {
display: grid;
grid-template-columns: 1fr;
grid-template-areas:
"header"
"player"
"queue";
}
.header {
grid-area: header;
}
.player {
grid-area: player;
}
.queue {
grid-area: queue;
}
@media (min-width: 992px) {
.app {
grid-template-columns: 2fr 3fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"header queue"
"player queue"
}
}
.entry {
cursor: pointer;
border: 1px solid black;
}
.entry:hover {
background-color: plum;
}
.entry.active {
background-color: plum;
}
</style>

View File

@ -1,45 +0,0 @@
<div>
<div bind:this={element}></div>
<button on:click={player.play()}>PLAY UWU</button>
</div>
<script>
import { onMount } from 'svelte';
import YoutubePlayer from 'yt-player'
export let videoId
export let playing
let element
let player
$: if (player && videoId) {
player.load(videoId, true)
}
$: if (player) {
if (playing) {
player.play()
} else {
player.pause()
}
}
onMount(() => {
player = new YoutubePlayer(element, {
autoplay: playing,
controls: true, // debug only
keyboard: false,
fullscreen: false,
modestBranding: true,
related: false
})
player.on('ended', () => console.log('ended u should select next entry now uwu'))
})
</script>
<style>
</style>

122
src/components/App.svelte Normal file
View File

@ -0,0 +1,122 @@
<main class="app">
<header class="header">
<h1>Eldritch Radio</h1>
</header>
<section class="viewer">
<Viewer></Viewer>
</section>
<section class="queue">
<div>
{#each $entries as entry}
<div class="entry" class:active={entry === $currentEntry} on:click={() => $currentEntry = entry}>
<div>{entry.metadata.title}</div>
<b>{entry.status.account.username} <small>{entry.status.account.acct}</small></b>
<small>{entry.tags}</small>
</div>
{/each}
</div>
<button on:click={() => entries.load(5)}>LOAD MOAR</button>
</section>
<section class="controls">
<Controls></Controls>
</section>
</main>
<script>
import { onMount } from 'svelte'
import Controls from '/components/Controls.svelte'
import Viewer from '/components/Viewer.svelte'
import { playing, loading, entry as currentEntry, entries } from '/store.js'
onMount(() => {
const unsub = entries.subscribe(async (xs) => {
if (xs.length) {
const [firstEntry] = xs
currentEntry.set(firstEntry)
unsub()
}
})
entries.load(7)
})
</script>
<style>
.app {
min-width: 100%;
min-height: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-areas:
"header"
"viewer"
"queue"
"controls";
}
.header {
grid-area: header;
position: sticky;
top: 0;
background: blueviolet;
color: whitesmoke;
padding: 0.4rem 0.8rem;
}
.header h1 {
margin: 0;
}
.viewer {
grid-area: viewer;
}
.queue {
grid-area: queue;
}
.controls {
grid-area: controls;
width: 100%;
position: sticky;
bottom: 0;
background: whitesmoke;
}
@media (min-width: 992px) {
.app {
grid-template-columns: 2fr 3fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header queue"
"viewer queue"
"controls controls"
}
}
.entry {
cursor: pointer;
border: 1px solid black;
}
.entry:hover {
background-color: rgb(219, 184, 219);
}
.entry.active {
background-color: plum;
}
</style>

View File

@ -0,0 +1,45 @@
<div class="controls">
<div class="controls-group">
<button on:click={() => $muted = !$muted}>
{#if $muted}
🔇
{:else}
{#if $volume < 20}
🔈
{:else if $volume < 80}
🔉
{:else}
🔊
{/if}
{/if}
</button>
<input type="range" min="0" max="100" bind:value={$volume}>
</div>
<div class="controls-group">
<button on:click={() => entry.previous()}>⏮️</button>
<button on:click={() => $playing = !$playing}>{#if $playing}⏸️{:else}▶️{/if}</button>
<button on:click={() => entry.next()}>⏭️</button>
</div>
<div class="controls-group">
<button></button>
<button>🔁</button>
</div>
</div>
<script>
import { playing, volume, muted, entry } from '/store.js'
</script>
<style>
.controls {
display: flex;
width: 100%;
}
.controls-group {
margin: 0 1rem;
}
</style>

View File

@ -0,0 +1,54 @@
<div>
<div bind:this={element}></div>
</div>
<script>
import { onMount } from 'svelte'
import { get } from 'svelte/store'
import YoutubePlayer from 'yt-player'
import { entry, playing, volume, muted } from '/store.js'
let element
let player
$: updateEntry($entry)
$: updatePlaying($playing)
$: updateVolume($volume)
$: updateMuted($muted)
const updateEntry = (entry) => {
if (player && entry) player.load(entry.id, get(playing))
}
const updatePlaying = (playing) => {
if (player) playing ? player.play() : player.pause()
}
const updateVolume = (volume) => {
if (player) player.setVolume(volume)
}
const updateMuted = (muted) => {
if (player) muted ? player.mute() : player.unMute()
}
onMount(() => {
player = new YoutubePlayer(element, {
width: 300,
height: 300,
autoplay: $playing,
controls: true, // debug only
keyboard: false,
fullscreen: false,
modestBranding: true,
related: false
})
player.on('ended', () => entry.next())
player.on('unplayable', () => entry.next())
})
</script>
<style>
</style>

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Eldritch Radio</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<script src="main.js"></script>

8
src/main.css Normal file
View File

@ -0,0 +1,8 @@
html, body {
width: 100%;
height: 100%;
}
body {
margin: 0;
}

View File

@ -1,4 +1,4 @@
import App from './App.svelte'
import App from '/components/App.svelte'
const app = new App({
target: document.body

View File

@ -1,5 +1,5 @@
import { writable, get } from 'svelte/store'
import * as util from './util.js'
import * as util from '/util.js'
export const domain = writable('eldritch.cafe')
@ -11,11 +11,10 @@ export const hashtags = writable([
])
export const playing = writable(true)
export const muted = writable(false)
export const volume = writable(100)
export const loading = writable(false)
export const entries = entriesStore(loading)
export const entries = entriesStore()
export const entry = entryStore(entries)
@ -26,8 +25,29 @@ function entryStore(entries) {
const store = writable(null)
const { set, update, subscribe } = store
const next = async () => {
const entriesList = await get(entries)
const select = (entry) => {
update(() => entry)
const entriesList = get(entries)
const index = entriesList.indexOf(entry)
if (index === entriesList.length - 1) {
entries.load(1)
}
}
const previous = () => {
const entriesList = get(entries)
update(currentEntry => {
const index = entriesList.indexOf(currentEntry)
return index > 0 ? entriesList[index - 1] : null
})
}
const next = () => {
const entriesList = get(entries)
update(oldEntry => {
if (entriesList.length === 0) {
@ -51,39 +71,16 @@ function entryStore(entries) {
})
}
return { subscribe, set, next }
return { subscribe, set: select, previous, next }
}
async function* loader(loading) {
loading.set(true)
let { statuses, nextLink, previousLink } = await util.fetchTimeline('https://eldritch.cafe/api/v1/timelines/tag/np')
loading.set(false)
yield* util.statusesToEntries(statuses)
while (true) {
loading.set(true)
const timeline = await util.fetchTimeline(nextLink)
loading.set(false)
nextLink = timeline.nextLink
yield* util.statusesToEntries(timeline.statuses)
}
}
function entriesStore(loading) {
const entriesSteam = loader(loading)
function entriesStore() {
const entriesSteam = util.statusesToEntries(util.statusesStreaming())
const store = writable([])
const { update, subscribe } = store
const load = async (number) => {
if (get(loading)) {
return
}
for (let i = 0; i < number; i++) {
const iteratorResult = await entriesSteam.next()
@ -97,4 +94,3 @@ function entriesStore(loading) {
return { subscribe, load }
}

View File

@ -1,4 +1,45 @@
import getUrls from 'get-urls'
import { pipe, asyncFilter, asyncMap, asyncTap, asyncTake } from 'iter-tools'
export async function* statusesStreaming() {
let { statuses, nextLink, previousLink } = await fetchTimeline('https://eldritch.cafe/api/v1/timelines/tag/np')
yield* statuses
while (nextLink) {
const a = await fetchTimeline(nextLink)
nextLink = a.nextLink
yield* a.statuses
}
}
export const statusesToEntries = pipe(
asyncMap(status => ({ status, urls: Array.from(getUrls(status.content)).filter(isSupportedUrl) })),
asyncFilter(entry => entry.urls.length > 0),
asyncMap(async ({ status, urls }) => {
const [url] = urls
const id = getYoutubeVideoId(url)
const tags = intersection(status.tags.map(tag => tag.name), [
'np',
'nowplaying',
'tootradio',
'pouetradio'
])
const metadata = await fetchYoutubeMetadata(id)
return { status, url, id, tags, metadata }
}),
asyncTake(20)
)
function fetchYoutubeMetadata(id) {
return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${id}`)
.then(response => response.json())
}
export function isSupportedUrl(urlAsString) {
const url = new URL(urlAsString)
@ -40,27 +81,4 @@ export function parseLinkHeader(link) {
}
return links
}
export function statusesToEntries(statuses) {
const entries = []
return statuses
.map(status => {
const [url] = Array.from(getUrls(status.content)).filter(isSupportedUrl)
return { status, url }
})
.filter(entry => entry.url != null)
.map(({ status, url }) => {
const id = getYoutubeVideoId(url)
const tags = intersection(status.tags.map(tag => tag.name), [
'np',
'nowplaying',
'tootradio',
'pouetradio'
])
return { status, url, id, tags }
})
}