mirror of
https://github.com/owntone/owntone-server.git
synced 2025-02-03 09:56:00 -05:00
[web-src] Generate cover image if not available for now playing page,
and some styling changes for the navbars
This commit is contained in:
parent
65b444f0a3
commit
4b1688ceb8
81
web-src/src/components/CoverPlaceholder.vue
Normal file
81
web-src/src/components/CoverPlaceholder.vue
Normal 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>
|
@ -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">
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
web-src/src/lib/SVGRenderer.js
Normal file
31
web-src/src/lib/SVGRenderer.js
Normal 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
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user