[web-src] Generate cover image if not available for now playing page,

and some styling changes for the navbars
This commit is contained in:
chme 2020-04-17 16:24:49 +02:00
parent 65b444f0a3
commit 4b1688ceb8
6 changed files with 202 additions and 106 deletions

View File

@ -0,0 +1,81 @@
<template>
<img :src="dataURI" :alt="alt_text">
</template>
<script>
import SVGRenderer from '@/lib/SVGRenderer'
import stringToColor from 'string-to-color'
export default {
name: 'CoverPlaceholder',
props: ['artist', 'album'],
data () {
return {
svg: new SVGRenderer(),
width: 600,
height: 600,
font_family: 'sans-serif',
font_size: 200,
font_weight: 600
}
},
computed: {
alt_text () {
return this.artist + ' - ' + this.album
},
caption () {
if (this.album) {
return this.album.substring(0, 2)
}
if (this.artist) {
return this.artist.substring(0, 2)
}
return ''
},
background_color () {
return stringToColor(this.alt_text)
},
is_background_light () {
// Based on https://stackoverflow.com/a/44615197
const hex = this.background_color.replace(/#/, '')
const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16)
const luma = [
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255
return luma > 0.5
},
text_color () {
return this.is_background_light ? '#000000' : '#ffffff'
},
rendererParams () {
return {
width: this.width,
height: this.height,
textColor: this.text_color,
backgroundColor: this.background_color,
caption: this.caption,
fontFamily: this.font_family,
fontSize: this.font_size,
fontWeight: this.font_weight
}
},
dataURI () {
return this.svg.render(this.rendererParams)
}
}
}
</script>

View File

@ -162,11 +162,16 @@
<!-- Outputs: stream volume --> <!-- Outputs: stream volume -->
<hr class="navbar-divider"> <hr class="navbar-divider">
<div class="navbar-item"> <div class="navbar-item fd-has-margin-bottom">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0;">
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a> <a class="button is-white is-small" :class="{ 'is-loading': loading }">
<span class="icon fd-has-action"
:class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }"
@click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i>
</span>
</a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">

View File

@ -133,14 +133,12 @@ export default {
methods: { methods: {
on_click_outside_settings () { on_click_outside_settings () {
console.log('yyyyy')
this.show_settings_menu = !this.show_settings_menu this.show_settings_menu = !this.show_settings_menu
} }
}, },
watch: { watch: {
$route (to, from) { $route (to, from) {
console.log('xxxx')
this.show_settings_menu = false this.show_settings_menu = false
} }
} }

View File

@ -0,0 +1,31 @@
/*
* SVGRenderer taken from https://github.com/bendera/placeholder published under MIT License
* Copyright (c) 2017 Adam Bender
* https://github.com/bendera/placeholder/blob/master/LICENSE
*/
class SVGRenderer {
render (data) {
const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' +
'<defs>' +
'<style type="text/css">' +
' #holder text {' +
' fill: ' + data.textColor + ';' +
' font-family: ' + data.fontFamily + ';' +
' font-size: ' + data.fontSize + 'px;' +
' font-weight: ' + data.fontWeight + ';' +
' }' +
' </style>' +
'</defs>' +
'<g id="holder">' +
' <rect width="100%" height="100%" fill="' + data.backgroundColor + '"></rect>' +
' <g>' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' + data.caption + '</text>' +
' </g>' +
'</g>' +
'</svg>'
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
}
}
export default SVGRenderer

View File

@ -86,21 +86,6 @@ a.navbar-item {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.fd-is-fullheight {
height: calc(100vh - 3.25rem - 3.25rem);
}
.fd-is-fullheight-body {
flex-shrink: 1;
overflow: hidden;
height: 100%
}
.fd-image-fullheight {
height: 100%;
width: auto;
}
.fd-tabs-section { .fd-tabs-section {
padding-bottom: 3px; padding-bottom: 3px;
padding-top: 3px; padding-top: 3px;
@ -123,6 +108,33 @@ section.fd-tabs-section + section.fd-content {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
} }
/* Now playing page */
.fd-is-fullheight {
height: calc(100vh - 3.25rem - 3.25rem);
display: flex;
flex-direction: column;
}
.fd-is-fullheight .fd-is-expanded {
max-height: calc(100vh - 25rem);
padding: 1.5rem;
overflow: hidden;
flex-grow: 1;
flex-shrink: 1;
}
.fd-cover-image {
height: 100%;
}
.fd-cover-image img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0px 0px 1px rgba(0,0,0,.3)) drop-shadow(0px 0px 10px rgba(0,0,0,.3));
}
.sortable-chosen .media-right { .sortable-chosen .media-right {
visibility: hidden; visibility: hidden;
} }

View File

@ -1,98 +1,66 @@
<template> <template>
<section class="fd-is-fullheight" style="display: flex; flex-direction: column;"> <section>
<div class="" style="max-height: calc(100vh - 25rem); padding: 1.5rem; overflow: hidden; flex-grow: 1;flex-shrink: 1;" v-show="artwork_visible"> <div v-if="now_playing.id > 0" class="fd-is-fullheight">
<img :src="artwork_url" <div class="fd-is-expanded">
class="fd-has-action" <figure @click="open_dialog(now_playing)" class="fd-cover-image">
style="width: 100%;height: 100%;object-fit: contain;filter: drop-shadow(0px 0px 1px rgba(0,0,0,.3)) drop-shadow(0px 0px 10px rgba(0,0,0,.3));" <img
@load="artwork_loaded" v-show="artwork_visible"
@error="artwork_error" :src="artwork_url"
@click="open_dialog(now_playing)"> class="fd-has-action"
</div> @load="artwork_loaded"
<div class="fd-has-padding-left-right"> @error="artwork_error">
<div class="container has-text-centered"> <cover-placeholder
<p class="control has-text-centered fd-progress-now-playing"> v-show="!artwork_visible"
<range-slider :artist="now_playing.artist"
class="seek-slider fd-has-action" :album="now_playing.album"
min="0" class="fd-has-action" />
:max="state.item_length_ms" </figure>
:value="item_progress_ms" </div>
:disabled="state.state === 'stop'" <div class="fd-has-padding-left-right">
step="1000" <div class="container has-text-centered">
@change="seek" > <p class="control has-text-centered fd-progress-now-playing">
</range-slider> <range-slider
</p> class="seek-slider fd-has-action"
<p class="content"> min="0"
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span> :max="state.item_length_ms"
</p> :value="item_progress_ms"
:disabled="state.state === 'stop'"
step="1000"
@change="seek" >
</range-slider>
</p>
<p class="content">
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span>
</p>
</div>
</div>
<div class="fd-has-padding-left-right">
<div class="container has-text-centered fd-has-margin-top">
<h1 class="title is-5">
{{ now_playing.title }}
</h1>
<h2 class="title is-6">
{{ now_playing.artist }}
</h2>
<h2 class="subtitle is-6 has-text-grey has-text-weight-bold" v-if="composer">
{{ composer }}
</h2>
<h3 class="subtitle is-6">
{{ now_playing.album }}
</h3>
</div>
</div> </div>
</div> </div>
<div class="fd-has-padding-left-right"> <div v-else class="fd-is-fullheight" style="justify-content: center;">
<div class="container has-text-centered fd-has-margin-top"> <div class="fd-is-expanded fd-has-padding-left-right has-text-centered">
<h1 class="title is-5"> <h1 class="title is-5">
{{ now_playing.title }} You play queue is empty
</h1> </h1>
<h2 class="title is-6">
{{ now_playing.artist }}
</h2>
<h2 class="subtitle is-6 has-text-grey has-text-weight-bold" v-if="composer">
{{ composer }}
</h2>
<h3 class="subtitle is-6">
{{ now_playing.album }}
</h3>
</div>
</div>
<!--
<div class="hero-head fd-has-padding-left-right">
<div class="container has-text-centered fd-has-margin-top">
<h1 class="title is-5">
{{ now_playing.title }}
</h1>
<h2 class="title is-6">
{{ now_playing.artist }}
</h2>
<h2 class="subtitle is-6 has-text-grey has-text-weight-bold" v-if="composer">
{{ composer }}
</h2>
<h3 class="subtitle is-6">
{{ now_playing.album }}
</h3>
</div>
</div>
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="artwork_visible">
<figure class="image is-square">
<img :src="artwork_url"
class="fd-has-shadow fd-image-fullheight fd-has-action"
style="width: auto; max-height: calc(100vh - 25rem); margin: 0 auto;"
@load="artwork_loaded"
@error="artwork_error"
@click="open_dialog(now_playing)">
</figure>
</div>
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="!artwork_visible">
<a @click="open_dialog(now_playing)" class="button is-white is-medium">
<span class="icon has-text-grey-light"><i class="mdi mdi-information-outline"></i></span>
</a>
</div>
<div class="hero-foot fd-has-padding-left-right">
<div class="container has-text-centered fd-has-margin-bottom">
<p class="control has-text-centered fd-progress-now-playing">
<range-slider
class="seek-slider fd-has-action"
min="0"
:max="state.item_length_ms"
:value="item_progress_ms"
:disabled="state.state === 'stop'"
step="1000"
@change="seek" >
</range-slider>
</p>
<p class="content"> <p class="content">
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span> Add some tracks by browsing your library
</p> </p>
</div> </div>
</div> </div>
-->
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" /> <modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
</section> </section>
</template> </template>
@ -100,12 +68,13 @@
<script> <script>
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem' import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
import RangeSlider from 'vue-range-slider' import RangeSlider from 'vue-range-slider'
import CoverPlaceholder from '@/components/CoverPlaceholder'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
export default { export default {
name: 'PageNowPlaying', name: 'PageNowPlaying',
components: { ModalDialogQueueItem, RangeSlider }, components: { ModalDialogQueueItem, RangeSlider, CoverPlaceholder },
data () { data () {
return { return {