kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Reimplement embedded player ui with petite-vue
rodzic
56a1058539
commit
86be283c6c
|
@ -83,7 +83,7 @@ http {
|
|||
proxy_pass http://funkwhale-front/front/;
|
||||
}
|
||||
location /front/embed.html {
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src https: http: 'self' 'unsafe-inline'; img-src https: http: 'self' data:; font-src https: http: 'self' data:; object-src 'none'; media-src https: http: 'self' data:";
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' unpkg.com 'unsafe-inline' 'unsafe-eval'; style-src https: http: 'self' 'unsafe-inline'; img-src https: http: 'self' data:; font-src https: http: 'self' data:; object-src 'none'; media-src https: http: 'self' data:";
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
add_header X-Frame-Options "" always;
|
||||
proxy_pass http://funkwhale-front/front/embed.html;
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="generator" content="Funkwhale">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>Funkwhale Widget</title>
|
||||
<script type="module" src="/src/embed/embed.ts"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but this widget doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,207 @@
|
|||
:root {
|
||||
--fw-darker: #1b1c1d;
|
||||
--fw-dark: #2f3030;
|
||||
--fw-light: #666666;
|
||||
--fw-primary: #f2711c;
|
||||
--fw-text: #fff;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
font-family: sans-serif;
|
||||
|
||||
background-color: var(--fw-darker);
|
||||
color: var(--fw-text);
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-rows: 166px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/*
|
||||
Player
|
||||
*/
|
||||
|
||||
.player {
|
||||
display: grid;
|
||||
grid-template-areas: 'cover content content' 'cover controls logo';
|
||||
grid-template-columns: 166px 1fr 50px;
|
||||
align-items: flex-end;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
grid-area: cover;
|
||||
background-color: var(--fw-dark);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.player-content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.player-controls {
|
||||
grid-area: controls;
|
||||
height: 36px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto 1fr auto 100px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--fw-primary);
|
||||
}
|
||||
|
||||
button.play {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
button > svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
display: block;
|
||||
aspect-ratio: 1;
|
||||
background-color: var(--fw-primary);
|
||||
padding: 4px;
|
||||
margin: 8px -8px -8px 8px;
|
||||
}
|
||||
|
||||
/*
|
||||
Track list
|
||||
*/
|
||||
|
||||
.track-list {
|
||||
background-color: var(--fw-dark);
|
||||
overflow-y: scroll;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: var(--entry-bg, transparent);
|
||||
}
|
||||
|
||||
tr.current {
|
||||
--entry-bg: var(--fw-darker);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
--entry-bg: var(--fw-light);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
padding-left: 16px;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Sliders
|
||||
*/
|
||||
|
||||
input[type=range] {
|
||||
background: transparent;
|
||||
appearance: none;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
--range-color: var(--fw-light);
|
||||
--range-size: 0.6em;
|
||||
|
||||
--min: 0;
|
||||
--max: 100;
|
||||
--value: 50;
|
||||
--range: calc(var(--max) - var(--min));
|
||||
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||
--sx: calc(0.5 * var(--range-size) + var(--ratio) * (100% - var(--range-size)));
|
||||
}
|
||||
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: var(--range-size);
|
||||
height: var(--range-size);
|
||||
border-radius: calc(var(--range-size) / 2);
|
||||
background: var(--range-color);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
width: var(--range-size);
|
||||
height: var(--range-size);
|
||||
border-radius: calc(var(--range-size) / 2);
|
||||
background: var(--range-color);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
appearance: none;
|
||||
height: var(--range-size);
|
||||
border: none;
|
||||
border-radius: calc(var(--range-size) / 2);
|
||||
box-shadow: none;
|
||||
background: linear-gradient(var(--range-color),var(--range-color)) 0/var(--sx) 100% no-repeat, var(--fw-dark);
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
appearance: none;
|
||||
height: var(--range-size);
|
||||
border: none;
|
||||
border-radius: calc(var(--range-size) / 2);
|
||||
box-shadow: none;
|
||||
background: linear-gradient(var(--range-color),var(--range-color)) 0/var(--sx) 100% no-repeat, var(--fw-dark);
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="generator" content="Funkwhale">
|
||||
|
||||
<link rel="icon" href="/favicon.png">
|
||||
|
||||
<title>Funkwhale Widget</title>
|
||||
|
||||
<link rel="stylesheet" href="embed.css">
|
||||
|
||||
<script type="module">
|
||||
import { createApp, reactive } from 'https://unpkg.com/petite-vue@0.4.1?module'
|
||||
|
||||
const params = new URL(location.href).searchParams
|
||||
const type = params.get('type')
|
||||
const id = params.get('id')
|
||||
|
||||
// Duration
|
||||
const ZERO_DATE = +new Date('2022-01-01T00:00:00.000')
|
||||
const intl = new Intl.DateTimeFormat('en', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23'
|
||||
})
|
||||
|
||||
const tracks = [
|
||||
{
|
||||
id: 8,
|
||||
title: 'Song name',
|
||||
artist: {
|
||||
name: 'Artist name'
|
||||
},
|
||||
sources: [{ duration: 6666 }]
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: 'Another song name',
|
||||
artist: {
|
||||
name: 'Another artist name'
|
||||
},
|
||||
album: {
|
||||
title: 'Another album title'
|
||||
},
|
||||
sources: [{ duration: 666 }]
|
||||
}
|
||||
]
|
||||
|
||||
// Player
|
||||
const player = reactive({
|
||||
playing: false,
|
||||
current: 0,
|
||||
seek: 0,
|
||||
play (index) {
|
||||
this.current = index
|
||||
},
|
||||
|
||||
togglePlay () {
|
||||
this.playing = !this.playing
|
||||
}
|
||||
})
|
||||
|
||||
// Volume
|
||||
const DEFAULT_VOLUME = 75
|
||||
const volume = reactive({
|
||||
level: DEFAULT_VOLUME,
|
||||
lastLevel: DEFAULT_VOLUME,
|
||||
|
||||
mute () {
|
||||
if (this.lastLevel === 0) {
|
||||
this.lastLevel = DEFAULT_VOLUME
|
||||
}
|
||||
|
||||
const lastLevel = this.level
|
||||
this.level = lastLevel === 0
|
||||
? this.lastLevel
|
||||
: 0
|
||||
|
||||
this.lastLevel = lastLevel
|
||||
}
|
||||
})
|
||||
|
||||
// Application
|
||||
const app = createApp({
|
||||
coverUrl: '',
|
||||
type,
|
||||
id,
|
||||
|
||||
player,
|
||||
volume,
|
||||
|
||||
tracks,
|
||||
|
||||
formatDuration (duration) {
|
||||
const time = intl.format(new Date(ZERO_DATE + duration * 1e3))
|
||||
return time.replace(/^00:/, '')
|
||||
}
|
||||
})
|
||||
|
||||
app.directive('range', (ctx) => {
|
||||
ctx.effect(() => {
|
||||
ctx.el.style.setProperty('--value', ctx.get())
|
||||
})
|
||||
})
|
||||
|
||||
app.mount()
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<template id="track-entry">
|
||||
|
||||
</template>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but this widget doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<main v-scope v-cloak>
|
||||
<div class="player">
|
||||
<img :src="coverUrl" class="cover-image" />
|
||||
|
||||
<div class="player-content">
|
||||
<h1>{{ tracks[player.current].title }}</h1>
|
||||
<h2>{{ tracks[player.current].artist.name }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="player-controls">
|
||||
<button>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-start-fill" viewBox="0 0 16 16">
|
||||
<path d="M4 4a.5.5 0 0 1 1 0v3.248l6.267-3.636c.54-.313 1.232.066 1.232.696v7.384c0 .63-.692 1.01-1.232.697L5 8.753V12a.5.5 0 0 1-1 0V4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="player.togglePlay" class="play">
|
||||
<svg v-if="!player.playing" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
|
||||
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
|
||||
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-end-fill" viewBox="0 0 16 16">
|
||||
<path d="M12.5 4a.5.5 0 0 0-1 0v3.248L5.233 3.612C4.693 3.3 4 3.678 4 4.308v7.384c0 .63.692 1.01 1.233.697L11.5 8.753V12a.5.5 0 0 0 1 0V4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<input
|
||||
v-model.number="player.seek"
|
||||
v-range="player.seek"
|
||||
type="range"
|
||||
step="0.1"
|
||||
/>
|
||||
|
||||
<button @click="volume.mute">
|
||||
<svg v-if="volume.level === 0" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-volume-mute-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zm7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-volume-up-fill" viewBox="0 0 16 16">
|
||||
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"/>
|
||||
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"/>
|
||||
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<input
|
||||
v-model.number="volume.level"
|
||||
v-range="volume.level"
|
||||
type="range"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
title="Funkwhale"
|
||||
href="https://funkwhale.audio"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="logo-link"
|
||||
>
|
||||
<img src="logo-white.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="track-list">
|
||||
<table>
|
||||
<tr
|
||||
v-for="(track, index) in tracks"
|
||||
:id="'queue-item-' + index"
|
||||
:key="track.id"
|
||||
role="button"
|
||||
:class="{ 'current': player.current === index }"
|
||||
@click="player.play(index)"
|
||||
>
|
||||
<td>
|
||||
{{ index + 1 }}
|
||||
</td>
|
||||
<td :title="track.title">
|
||||
{{ track.title }}
|
||||
</td>
|
||||
<td :title="track.artist.name">
|
||||
{{ track.artist.name }}
|
||||
</td>
|
||||
<td :title="track.album?.title">
|
||||
{{ track.album?.title }}
|
||||
</td>
|
||||
<td>
|
||||
{{ formatDuration(track.sources[0].duration) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,39 @@
|
|||
<svg
|
||||
id="layer_1"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 141.7 141.7"
|
||||
enable-background="new 0 0 141.7 141.7"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11
|
||||
c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1
|
||||
c0,17.1-13.9,31-31,31c-17.1,0-31-13.9-31-31c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C29.6,87.6,48.1,106.1,70.9,106.1z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1
|
||||
c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,33.8,27.5,61.3,61.3,61.3c33.8,0,61.3-27.5,61.3-61.3
|
||||
C132.2,64.3,131.7,63.8,131.1,63.8z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M43.3,37.3c4.1,2.1,8.5,2.5,12.5,4.8c2.6,1.5,4.2,3.2,5.8,5.7c2.5,3.8,2.4,8.5,2.4,8.5l0.3,5.2
|
||||
c0,0,2,5.2,6.4,5.2c4.7,0,6.4-5.2,6.4-5.2l0.3-5.2c0,0-0.1-4.7,2.4-8.5c1.6-2.5,3.2-4.3,5.8-5.7c4-2.3,8.4-2.7,12.5-4.8
|
||||
c4.1-2.1,8.1-4.8,10.8-8.6c2.7-3.8,4-8.8,2.5-13.2c-7.8-0.4-16.8,0.5-23.7,4.2c-9.6,5.1-15.4,3.3-17.1,10.9h-0.1
|
||||
c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 1.6 KiB |
|
@ -40,14 +40,6 @@ export default defineConfig(({ mode }) => ({
|
|||
'~': resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, './index.html'),
|
||||
embed: resolve(__dirname, './embed.html')
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true
|
||||
|
|
Ładowanie…
Reference in New Issue