Skip to content
Snippets Groups Projects
Verified Commit 815d7293 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Fix #578: added embed.html page to power iframe widget

parent 10e630f3
No related branches found
No related tags found
No related merge requests found
Allow embedding of albums and tracks available in public libraries via an <iframe> (#578)
Iframe widget to embed public tracks and albums [manual action required]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Funkwhale now support embedding a lightweight audio player on external websites
for album and tracks that are available in public libraries. Important pages,
such as artist, album and track pages also include OpenGraph tags that will
enable previews on compatible apps (like sharing a Funkwhale track link on Mastodon
or Twitter).
To achieve that, we had to tweak the way Funkwhale front-end is served. You'll have
to modify your nginx configuration when upgrading to keep your instance working.
**On docker setups**, edit your ``/srv/funkwhale/nginx/funkwhale.template`` and replace
the ``location /api/`` and `location /` blocks by the following snippets::
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
location /front/ {
alias /frontend;
}
The change of configuration will be picked when restarting your nginx container.
**On non-docker setups**, edit your ``/etc/nginx/sites-available/funkwhale.conf`` file,
and replace the ``location /api/`` and `location /` blocks by the following snippets::
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
location /front/ {
alias ${FUNKWHALE_FRONTEND_PATH};
}
Replace ``${FUNKWHALE_FRONTEND_PATH}`` by the corresponding variable from your .env file,
which should be ``/srv/funkwhale/front/dist`` by default, then reload your nginx process with
``sudo systemctl reload nginx``.
......@@ -10,20 +10,13 @@ services:
- "HOST=0.0.0.0"
- "VUE_PORT=${VUE_PORT-8080}"
ports:
- "${VUE_PORT-8080}:${VUE_PORT-8080}"
- "${VUE_PORT-8080}"
volumes:
- "./front:/app"
- "/app/node_modules"
- "./po:/po"
networks:
- federation
- internal
labels:
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
traefik.enable: "true"
traefik.federation.protocol: "http"
traefik.federation.port: "${VUE_PORT-8080}"
postgres:
env_file:
......@@ -66,7 +59,7 @@ services:
- "CACHE_URL=redis://redis:6379/0"
volumes:
- ./api:/app
- "${MUSIC_DIRECTORY-./data/music}:/music:ro"
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
networks:
- internal
api:
......@@ -76,10 +69,10 @@ services:
build:
context: ./api
dockerfile: docker/Dockerfile.test
command: python /app/manage.py runserver 0.0.0.0:12081
command: python /app/manage.py runserver 0.0.0.0:${FUNKWHALE_API_PORT-5000}
volumes:
- ./api:/app
- "${MUSIC_DIRECTORY-./data/music}:/music:ro"
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
environment:
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
......@@ -99,22 +92,35 @@ services:
- .env
image: nginx
environment:
- "VUE_PORT=${VUE_PORT-8080}"
- "NGINX_MAX_BODY_SIZE=${NGINX_MAX_BODY_SIZE-30M}"
- "FUNKWHALE_API_IP=${FUNKHALE_API_IP-api}"
- "FUNKWHALE_API_PORT=${FUNKWHALE_API_PORT-5000}"
- "FUNKWHALE_FRONT_IP=${FUNKHALE_FRONT_IP-front}"
- "FUNKWHALE_FRONT_PORT=${VUE_PORT-8080}"
- "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME- }"
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
links:
- api
- front
volumes:
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
- ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
- "${MUSIC_DIRECTORY-./data/music}:/music:ro"
- ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf.template:ro
- ./api/funkwhale_api/media:/protected/media
ports:
- "6001"
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
- ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
- "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
networks:
- federation
- internal
labels:
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
traefik.enable: "true"
traefik.federation.protocol: "http"
traefik.federation.port: "80"
traefik.frontend.passHostHeader: true
traefik.docker.network: federation
docs:
build: docs
command: python serve.py
......
......@@ -32,26 +32,57 @@ http {
'' close;
}
upstream funkwhale-api {
server ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT};
}
upstream funkwhale-front {
server ${FUNKWHALE_FRONT_IP}:${FUNKWHALE_FRONT_PORT};
}
server {
listen 6001;
listen 80;
charset utf-8;
client_max_body_size 30M;
include /etc/nginx/funkwhale_proxy.conf;
location /_protected/media {
internal;
alias /protected/media;
location /front/ {
proxy_pass http://funkwhale-front/front/;
}
location /_protected/music {
internal;
alias /music;
location /front-server/ {
proxy_pass http://funkwhale-front/;
}
location / {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://api:12081/;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
# You can comment this if you do not plan to use the Subsonic API
location /rest/ {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://api:12081/api/subsonic/rest/;
proxy_pass http://funkwhale-api/api/subsonic/rest/;
}
location /media/ {
alias /protected/media/;
}
location /_protected/media {
# this is an internal location that is used to serve
# audio files once correct permission / authentication
# has been checked on API side
internal;
alias /protected/media;
}
location /_protected/music {
# this is an internal location that is used to serve
# audio files once correct permission / authentication
# has been checked on API side
# Set this to the same value as your MUSIC_DIRECTORY_PATH setting
internal;
alias /music;
}
}
}
#!/bin/bash -eux
FORWARDED_PORT="$VUE_PORT"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME// /}"
if [ -n "$COMPOSE_PROJECT_NAME" ]; then
echo
FUNKWHALE_HOSTNAME="$COMPOSE_PROJECT_NAME.funkwhale.test"
FORWARDED_PORT="443"
fi
echo "Copying template file..."
cp /etc/nginx/funkwhale_proxy.conf{.template,}
sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header Host \$host/proxy_set_header Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header X-Forwarded-Proto \$scheme/proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO}/" /etc/nginx/funkwhale_proxy.conf
cat /etc/nginx/funkwhale_proxy.conf
nginx -g "daemon off;"
envsubst "`env | awk -F = '{printf \" $$%s\", $$1}'`" \
< /etc/nginx/nginx.conf.template \
> /etc/nginx/nginx.conf \
&& cat /etc/nginx/nginx.conf \
&& nginx-debug -g 'daemon off;'
......@@ -27,6 +27,7 @@
"vue-gettext": "^2.1.0",
"vue-lazyload": "^1.2.6",
"vue-masonry": "^0.11.5",
"vue-plyr": "^5.0.4",
"vue-router": "^3.0.1",
"vue-upload-component": "^2.8.11",
"vuedraggable": "^2.16.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">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title>Funkwhale Widget</title>
</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>
<template>
<main :class="[theme]">
<!-- SVG from https://cdn.plyr.io/3.4.7/plyr.svg -->
<svg aria-hidden="true" style="display: none" xmlns="http://www.w3.org/2000/svg">
<symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z"/></symbol>
<symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z"/></symbol>
<symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z"/></symbol>
<symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z"/></symbol>
<symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol>
<symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z"/></symbol>
<symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z"/><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z"/></symbol>
<symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"/></symbol>
<symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z"/></symbol>
<symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"/></symbol>
<symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol>
<symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z"/><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol></svg>
<!-- those ones are from fork-awesome -->
<symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z"/></symbol>
<symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z"/></symbol>
</svg>
<article>
<aside class="cover main" v-if="currentTrack">
<img height="120" v-if="currentTrack.cover" :src="currentTrack.cover" alt="Cover" />
<img height="120" v-else src="./assets/embed/default-cover.jpeg" alt="Cover" />
</aside>
<div class="content" aria-label="Track information">
<header v-if="currentTrack">
<h3><a :href="fullUrl('/library/tracks/' + currentTrack.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.title }}</a></h3>
By <a :href="fullUrl('/library/artists/' + currentTrack.artist.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.artist.name }}</a>
</header>
<section v-if="!isLoading" class="controls" aria-label="Audio player">
<template v-if="currentTrack && currentTrack.sources.length > 0">
<div class="queue-controls plyr--audio" v-if="tracks.length > 1">
<div class="plyr__controls">
<button
@focus="setControlFocus($event, true)"
@blur="setControlFocus($event, false)"
@click="previous()"
type="button"
class="plyr__control"
aria-label="Play previous track">
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
<use xlink:href="#plyr-step-backward"></use>
</svg>
</button>
<button
@click="next()"
@focus="setControlFocus($event, true)"
@blur="setControlFocus($event, false)"
type="button"
class="plyr__control"
aria-label="Play next track">
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
<use xlink:href="#plyr-step-forward"></use>
</svg>
</button>
</div>
</div>
<vue-plyr
:key="currentIndex"
ref="player"
class="player"
:options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration}">
<audio preload="none">
<source v-for="source in currentTrack.sources" :src="source.src" :type="source.type"/>
</audio>
</vue-plyr>
</template>
<div v-else class="player">
<span v-if="error === 'invalid_type'" class="error">Widget improperly configured (bad resource type {{ type }}).</span>
<span v-else-if="error === 'invalid_id'" class="error">Widget improperly configured (missing resource id).</span>
<span v-else-if="error === 'server_not_found'" class="error">Track not found.</span>
<span v-else-if="error === 'server_requires_auth'" class="error">You need to login to access this resource.</span>
<span v-else-if="error === 'server_error'" class="error">A server error occured.</span>
<span v-else-if="error === 'server_error'" class="error">An unknown error occured while loading track data from server.</span>
<span v-else-if="currentTrack && currentTrack.sources.length === 0" class="error">This track is unavailable.</span>
<span v-else class="error">An unknown error occured while loading track data.</span>
</div>
<a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-wrapper">
<logo :fill="currentTheme.textColor" class="logo"></logo>
</a>
</section>
</div>
</article>
<div v-if="tracks.length > 1" class="queue-wrapper" id="queue">
<table class="queue">
<tbody>
<tr
:id="'queue-item-' + index"
role="button"
tabindex="0"
v-if="track.sources.length > 0"
:key="index"
:class="[{active: index === currentIndex}]"
@click="play(index)"
@keyup.enter="play(index)"
v-for="(track, index) in tracks">
<td class="position-cell" width="40">
<span class="position">
{{ index + 1 }}
</span>
</td>
<td class="title" :title="track.title" ><div colspan="2" class="ellipsis">{{ track.title }}</div></td>
<td class="artist" :title="track.artist.name" ><div class="ellipsis">{{ track.artist.name }}</div></td>
<td class="album">
<div class="ellipsis " v-if="track.album" :title="track.album.title">{{ track.album.title }}</div>
</td>
<td width="50">{{ time.durationFormatted(track.sources[0].duration) }}</td>
</tr>
</tbody>
</table>
</div>
</main>
</template>
<script>
import axios from 'axios'
import Logo from "@/components/Logo"
import url from '@/utils/url'
import time from '@/utils/time'
function getURLParams () {
var urlParams
var match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
urlParams = {};
while (match = search.exec(query))
urlParams[decode(match[1])] = decode(match[2]);
return urlParams
}
export default {
name: 'app',
components: {Logo},
data () {
return {
time,
supportedTypes: ['track', 'album'],
baseUrl: '',
error: null,
type: null,
id: null,
tracks: [],
url: null,
isLoading: true,
theme: 'dark',
currentIndex: -1,
themes: {
dark: {
textColor: 'white',
}
}
}
},
created () {
let params = getURLParams()
this.type = params.type
if (this.supportedTypes.indexOf(this.type) === -1) {
this.error = 'invalid_type'
}
this.id = params.id
if (!this.id) {
this.error = 'invalid_id'
}
if (this.error) {
this.isLoading = false
return
}
if (!!params.instance) {
this.baseUrl = params.instance
}
this.fetch(this.type, this.id)
},
mounted () {
var parser = document.createElement('a')
parser.href = this.baseUrl
this.url = parser
},
computed: {
currentTrack () {
if (this.tracks.length === 0) {
return null
}
return this.tracks[this.currentIndex]
},
currentTheme () {
return this.themes[this.theme]
},
controls () {
return [
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'mute', // Toggle mute
'volume', // Volume control
]
},
hasPrevious () {
return this.currentIndex > 0
},
hasNext () {
return this.currentIndex < this.tracks.length - 1
},
},
methods: {
next () {
if (this.hasNext) {
this.play(this.currentIndex + 1)
}
},
previous () {
if (this.hasPrevious) {
this.play(this.currentIndex - 1)
}
},
setControlFocus(event, enable) {
if (enable) {
event.target.classList.add("plyr__tab-focus");
} else {
event.target.classList.remove("plyr__tab-focus");
}
},
fetch (type, id) {
if (type === 'track') {
this.fetchTrack(id)
}
if (type === 'album') {
this.fetchTracks({album: id, playable: true})
}
},
play (index) {
this.currentIndex = index
let self = this
this.$nextTick(() => {
self.$refs.player.player.play()
})
},
fetchTrack (id) {
let self = this
let url = `${this.baseUrl}/api/v1/tracks/${id}/`
axios.get(url).then(response => {
self.tracks = self.parseTracks([response.data])
self.isLoading = false;
}).catch(error => {
if (error.response) {
console.log(error.response)
if (error.response.status === 404) {
self.error = 'server_not_found'
}
else if (error.response.status === 403) {
self.error = 'server_requires_auth'
}
else if (error.response.status === 500) {
self.error = 'server_error'
}
else {
self.error = 'server_unknown_error'
}
} else {
self.error = 'server_unknown_error'
}
self.isLoading = false;
})
},
fetchTracks (filters) {
let self = this
let url = `${this.baseUrl}/api/v1/tracks/`
axios.get(url, {params: filters}).then(response => {
self.tracks = self.parseTracks(response.data.results)
self.isLoading = false;
}).catch(error => {
if (error.response) {
console.log(error.response)
if (error.response.status === 404) {
self.error = 'server_not_found'
}
else if (error.response.status === 403) {
self.error = 'server_requires_auth'
}
else if (error.response.status === 500) {
self.error = 'server_error'
}
else {
self.error = 'server_unknown_error'
}
} else {
self.error = 'server_unknown_error'
}
self.isLoading = false;
})
},
parseTracks (tracks) {
let self = this
return tracks.map(t => {
return {
id: t.id,
title: t.title,
artist: t.artist,
album: t.album,
cover: self.getCover(t.album.cover),
sources: self.getSources(t.uploads)
}
})
},
bindEvents () {
let self = this
this.$refs.player.player.on('ended', () => {
self.next()
})
},
fullUrl (path) {
if (path.startsWith('/')) {
return this.baseUrl + path
}
return path
},
getCover(albumCover) {
if (albumCover) {
return albumCover.medium_square_crop
}
},
getSources (uploads) {
let self = this;
let sources = uploads.map(u => {
return {
type: u.mimetype,
src: self.fullUrl(u.listen_url),
duration: u.duration
}
})
if (sources.length > 0) {
// We always add a transcoded MP3 src at the end
// because transcoding is expensive, but we want browsers that do
// not support other codecs to be able to play it :)
sources.push({
type: 'audio/mpeg',
src: url.updateQueryString(
self.fullUrl(sources[0].src),
'to',
'mp3'
)
})
}
return sources
}
},
watch: {
currentIndex (v) {
// we bind player events
let self = this
this.$nextTick(() => {
self.bindEvents()
if (self.tracks.length > 0) {
var topPos = document.getElementById(`queue-item-${v}`).offsetTop;
document.getElementById('queue').scrollTop = topPos-10;
}
})
},
tracks () {
this.currentIndex = 0
}
}
}
</script>
<style lang="scss">
html,
body,
main {
height: 100%;
}
body {
margin: 0;
font-family: sans-serif;
}
main {
display: flex;
flex-direction: column;
}
article {
display: flex;
position: relative;
aside {
padding: 0.5em;
}
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
section.controls {
display: flex;
}
.cover {
max-width: 120px;
max-height: 120px;
}
.player {
flex: 1;
align-self: flex-end;
}
article .content {
flex: 1;
display: flex;
flex-direction: column;
h3 {
margin: 0 0 0.5em;
}
header {
flex: 1;
padding: 1em;
}
}
.player,
.queue-controls {
padding: 0.25em 0;
margin-right: 0.25em;
align-self: center;
}
section .plyr--audio .plyr__controls {
padding: 0;
}
.error {
font-weight: bold;
display: block;
text-align: center;
}
.logo-wrapper {
height: 2em;
width: 2em;
padding: 0.25em;
margin-left: 0.5em;
display: block;
}
[role="button"] {
cursor: pointer;
}
.ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.queue-wrapper {
flex: 1;
overflow-y: auto;
padding: 0.5em;
}
.queue {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-bottom: 0.5em;
td {
padding: 0.5em;
font-size: 90%;
img {
vertical-align: middle;
margin-right: 1em;
}
}
td:last-child {
text-align: right;
}
.position {
padding: 0.1em 0.3em;
display: inline-block;
}
}
@media screen and (max-width: 640px) {
.queue .album {
display: none;
}
.plyr__controls .plyr__time {
display: none;
}
}
@media screen and (max-width: 460px) {
article,
article .content {
display: block;
}
.cover.main {
float: right;
img {
height: 60px;
width: 60px;
}
}
}
@media screen and (max-width: 320px) {
.logo-wrapper,
.position-cell {
display: none;
}
}
// themes
.dark {
$primary-color: rgb(242, 113, 28);
$dark: rgb(27, 28, 29);
$lighter: rgb(47, 48, 48);
$clear: rgb(242, 242, 242);
// $primary-color: rgb(255, 88, 78);
.logo-wrapper {
background-color: $primary-color;
}
.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded="true"] {
background-color: $primary-color;
}
.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded="true"] {
background-color: $primary-color;
}
.plyr--full-ui input[type="range"] {
color: $primary-color;
}
article,
.player,
.plyr--audio .plyr__controls {
background-color: $dark;
}
.queue-wrapper {
background-color: $lighter;
}
article,
article a,
.player,
.queue tr,
.plyr--audio .plyr__controls {
color: white;
}
.plyr__control.plyr__tab-focus {
-webkit-box-shadow: 0 0 0 2px rgba(26, 175, 255, 0.5);
box-shadow: 0 0 0 2px rgba(26, 175, 255, 0.5);
outline: 0;
}
tr:hover,
tr:focus {
background-color: $dark;
}
tr.active {
background-color: $clear;
color: $dark;
}
tr.active {
.position {
background-color: $primary-color;
color: $clear;
}
}
}
</style>
front/src/assets/embed/default-cover.jpeg

8.78 KiB

......@@ -3,16 +3,16 @@
viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve">
<g>
<g>
<path fill="#4082B4" 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
<path :fill="fill" 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="#4082B4" 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
<path :fill="fill" 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="#4082B4" 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
<path :fill="fill" 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="#222222" 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
<path :fill="fill" 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"/>
......@@ -24,10 +24,8 @@
<script>
export default {
props: {
fill: {type: String, default: '#222222'}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>
import Vue from 'vue'
import Embed from './Embed'
import axios from 'axios'
import VuePlyr from 'vue-plyr'
Vue.use(VuePlyr)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
template: '<Embed/>',
components: { Embed }
})
......@@ -12,5 +12,13 @@ export default {
min = Math.floor(sec / 60)
sec = sec - min * 60
return pad(min) + ':' + pad(sec)
},
durationFormatted (v) {
let duration = parseInt(v)
if (duration % 1 !== 0) {
return time.parse(0)
}
duration = Math.round(duration)
return this.parse(duration)
}
}
module.exports = {
baseUrl: '/front/',
pages: {
embed: {
entry: 'src/embed.js',
template: 'public/embed.html',
filename: 'embed.html',
},
index: {
entry: 'src/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
chainWebpack: config => {
config.optimization.delete('splitChunks')
},
configureWebpack: {
resolve: {
alias: {
......@@ -9,33 +25,7 @@ module.exports = {
},
devServer: {
disableHostCheck: true,
proxy: {
'^/rest': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/staticfiles': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/.well-known': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/media': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/federation': {
target: 'http://nginx:6001',
changeOrigin: true,
ws: true,
},
'^/api': {
target: 'http://nginx:6001',
changeOrigin: true,
ws: true,
},
}
// use https://node1.funkwhale.test/front-server/ if you use docker with federation
public: process.env.FRONT_DEVSERVER_URL || ('http://localhost:' + (process.env.VUE_PORT || '8080'))
}
}
......@@ -2168,6 +2168,11 @@ core-js@^2.4.0, core-js@^2.5.3:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
core-js@^2.5.7:
version "2.6.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4"
integrity sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
......@@ -2430,6 +2435,11 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
custom-event-polyfill@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.6.tgz#6b026e81cd9f7bc896bd6b016a427407bb068db1"
integrity sha512-3FxpFlzGcHrDykwWu+xWVXZ8PfykM/9/bI3zXb953sh+AjInZWcQmrnmvPoZgiqNjmbtTm10PWvYqvRW527x6g==
cyclist@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
......@@ -4602,6 +4612,11 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
emojis-list "^2.0.0"
json5 "^0.5.0"
loadjs@^3.5.4:
version "3.5.5"
resolved "https://registry.yarnpkg.com/loadjs/-/loadjs-3.5.5.tgz#2fbaa981ffdd079e0f8786ea75aeed643483b368"
integrity sha512-qBuLnKt4C6+vctutozFqPHQ6s4SSa9tcE64NsvDJ92UZmUrFvqGI1oVOtnZz2xwpgOT+2niQtHtQIDP4e/wlTA==
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
......@@ -5677,6 +5692,17 @@ pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
plyr@^3.4.5:
version "3.4.7"
resolved "https://registry.yarnpkg.com/plyr/-/plyr-3.4.7.tgz#7d92470fb27f8019422c6d4edfd3b172d902ef06"
integrity sha512-RxxT2WdC4/sEZQT7CBZqKx5ImVw96aWjT6kB6DM82jy9GcWDiBBnv04m/AeeaXg9S5ambPdiHhB6Pzfm2q84Gw==
dependencies:
core-js "^2.5.7"
custom-event-polyfill "^1.0.6"
loadjs "^3.5.4"
raven-js "^3.27.0"
url-polyfill "^1.1.0"
pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
......@@ -6246,6 +6272,11 @@ raven-js@^3.26.4:
version "3.26.4"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.4.tgz#32aae3a63a9314467a453c94c89a364ea43707be"
raven-js@^3.27.0:
version "3.27.0"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.0.tgz#9f47c03e17933ce756e189f3669d49c441c1ba6e"
integrity sha512-vChdOL+yzecfnGA+B5EhEZkJ3kY3KlMzxEhShKh6Vdtooyl0yZfYNFQfYzgMf2v4pyQa+OTZ5esTxxgOOZDHqw==
raw-body@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
......@@ -7558,6 +7589,11 @@ url-parse@^1.1.8, url-parse@^1.4.3:
querystringify "^2.0.0"
requires-port "^1.0.0"
url-polyfill@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.3.tgz#ce0bdf2e923aa6f66bc198ab776323dfc5a91e62"
integrity sha512-xIAXc0DyXJCd767sSeRu4eqisyYhR0z0sohWArCn+WPwIatD39xGrc09l+tluIUi6jGkpGa8Gz8TKwkKYxMQvQ==
url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
......@@ -7682,6 +7718,13 @@ vue-masonry@^0.11.5:
masonry-layout "4.2.0"
vue "^2.0.0"
vue-plyr@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/vue-plyr/-/vue-plyr-5.0.4.tgz#13083b71a876d01200a3c93ebfd11585b671afda"
integrity sha512-zOLD7SZiYR/8DPYkZZR9zGTV+04GAc+fhnBymAWSRryncAG4889cYxXJSbIvlsNVGpdGRIOSIZ4p6pIupfmZ5w==
dependencies:
plyr "^3.4.5"
vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment