Merge pull request #1008 from chme/web_next

Player web interface v0.7.2
This commit is contained in:
Christian Meffert 2020-07-05 10:59:15 +02:00 committed by GitHub
commit 3fd812ef2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 4380 additions and 3921 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2060,8 +2060,6 @@ queue_item_to_json(struct db_queue_item *queue_item, char shuffle)
json_object *item;
char uri[100];
char artwork_url[100];
char chbuf[6];
const char *ch;
int ret;
item = json_object_new_object();
@ -2141,15 +2139,7 @@ queue_item_to_json(struct db_queue_item *queue_item, char shuffle)
safe_json_add_string(item, "type", queue_item->type);
json_object_object_add(item, "bitrate", json_object_new_int(queue_item->bitrate));
json_object_object_add(item, "samplerate", json_object_new_int(queue_item->samplerate));
switch (queue_item->channels)
{
case 1: ch = "mono"; break;
case 2: ch = "stereo"; break;
default:
snprintf(chbuf, sizeof(chbuf), "%d ch", queue_item->channels);
ch = chbuf;
}
safe_json_add_string(item, "channels", ch);
json_object_object_add(item, "channels", json_object_new_int(queue_item->channels));
return item;
}

7153
web-src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "forked-daapd-web",
"version": "0.7.1",
"version": "0.7.2",
"private": true,
"description": "forked-daapd web interface",
"author": "chme <christian.meffert@googlemail.com>",
@ -12,36 +12,39 @@
},
"dependencies": {
"axios": "^0.19.2",
"bulma": "^0.8.2",
"bulma": "^0.9.0",
"core-js": "^3.6.5",
"mdi": "^2.2.43",
"moment": "^2.24.0",
"moment": "^2.27.0",
"moment-duration-format": "^2.3.2",
"npm": "^6.14.4",
"npm": "^6.14.5",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.2.0",
"string-to-color": "^2.1.3",
"spotify-web-api-js": "^1.4.0",
"string-to-color": "^2.1.4",
"v-click-outside": "^3.0.1",
"vue": "^2.6.11",
"vue-infinite-loading": "^2.4.5",
"vue-progressbar": "^0.7.5",
"vue-range-slider": "^0.6.0",
"vue-router": "^3.1.6",
"vue-router": "^3.3.4",
"vue-tiny-lazyload-img": "^0.1.0",
"vuedraggable": "^2.23.2",
"vuex": "^3.1.3"
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-service": "^4.3.1",
"@vue/cli-plugin-babel": "^4.4.6",
"@vue/cli-plugin-eslint": "^4.4.6",
"@vue/cli-service": "^4.4.6",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.2",
"eslint": "^7.3.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.9",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
},
"license": "GPL-2.0"

View File

@ -9,8 +9,7 @@
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" />
<notifications v-show="!show_burger_menu" />
<navbar-bottom />
<div class="is-overlay" v-show="show_burger_menu || show_player_menu"
style="z-index:25; width: 100vw; height:100vh;background-color: rgba(10, 10, 10, 0.2);"
<div class="fd-overlay-fullscreen" v-show="show_burger_menu || show_player_menu"
@click="show_burger_menu = show_player_menu = false"></div>
</div>
</template>

View File

@ -1,15 +1,9 @@
<template>
<figure>
<img
v-show="artwork_visible"
:src="artwork_url_with_size"
@load="artwork_loaded"
@error="artwork_error"
@click="$emit('click')">
<img
v-show="!artwork_visible"
<img v-lazyload
:src="dataURI"
:alt="alt_text"
:data-src="artwork_url_with_size"
:data-err="dataURI"
@click="$emit('click')">
</figure>
</template>
@ -30,9 +24,7 @@ export default {
height: 600,
font_family: 'sans-serif',
font_size: 200,
font_weight: 600,
artwork_visible: false
font_weight: 600
}
},
@ -95,16 +87,6 @@ export default {
dataURI () {
return this.svg.render(this.rendererParams)
}
},
methods: {
artwork_loaded: function () {
this.artwork_visible = true
},
artwork_error: function () {
this.artwork_visible = false
}
}
}
</script>

View File

@ -1,10 +1,13 @@
<template functional>
<div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()">
<slot name="artwork"></slot>
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ props.album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artist }}</b></h2>
<div style="margin-top:0.7rem;">
<h1 class="title is-6">{{ props.album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artist }}</b></h2>
</div>
</div>
<div class="media-right">
<div class="media-right" style="padding-top:0.7rem;">
<slot name="actions"></slot>
</div>
</div>

View File

@ -53,7 +53,12 @@
</p>
<p>
<span class="heading">Quality</span>
<span class="title is-6">{{ item.type}} | {{ item.samplerate }} Hz | {{ item.channels }} | {{ item.bitrate }} Kb/s</span>
<span class="title is-6">
{{ item.type }}
<span v-if="item.samplerate"> | {{ item.samplerate }} Hz</span>
<span v-if="item.channels"> | {{ item.channels | channels }}</span>
<span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span>
</span>
</p>
</div>
</div>

View File

@ -31,7 +31,7 @@
</p>
<p v-if="track.date_released">
<span class="heading">Release date</span>
<span class="title is-6">{{ track.date_released | time('L')}}</span>
<span class="title is-6">{{ track.date_released | time('L') }}</span>
</p>
<p v-else-if="track.year > 0">
<span class="heading">Year</span>
@ -59,7 +59,12 @@
</p>
<p>
<span class="heading">Quality</span>
<span class="title is-6">{{ track.type}} | {{ track.samplerate}} Hz | {{ track.channels }} channels | {{ track.bitrate}} Kb/s</span>
<span class="title is-6">
{{ track.type }}
<span v-if="track.samplerate"> | {{ track.samplerate }} Hz</span>
<span v-if="track.channels"> | {{ track.channels | channels }}</span>
<span v-if="track.bitrate"> | {{ track.bitrate }} Kb/s</span>
</span>
</p>
<p>
<span class="heading">Added at</span>

View File

@ -49,6 +49,7 @@
<navbar-item-link to="/music/artists"><span class="fd-navbar-item-level2">Artists</span></navbar-item-link>
<navbar-item-link to="/music/albums"><span class="fd-navbar-item-level2">Albums</span></navbar-item-link>
<navbar-item-link to="/music/genres"><span class="fd-navbar-item-level2">Genres</span></navbar-item-link>
<navbar-item-link to="/music/radio"><span class="fd-navbar-item-level2">Radio</span></navbar-item-link>
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link>
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link>
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link>
@ -56,9 +57,6 @@
<navbar-item-link to="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link>
<hr class="fd-navbar-divider">
<a class="navbar-item" href="/admin.html">Admin</a>
<hr class="fd-navbar-divider">
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
<navbar-item-link to="/about">About</navbar-item-link>

View File

@ -3,7 +3,7 @@
<div class="media-content fd-has-action is-clipped" v-on:click="open_album">
<h1 class="title is-6">{{ album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ album.release_date }})</h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ album.release_date | time('L') }})</h2>
</div>
<div class="media-right">
<slot name="actions"></slot>

View File

@ -19,7 +19,7 @@
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
<span class="title is-6">{{ album.release_date | time('L') }}</span>
</p>
<p>
<span class="heading">Type</span>

View File

@ -23,7 +23,7 @@
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
<span class="title is-6">{{ album.release_date | time('L') }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>

View File

@ -29,6 +29,12 @@
<span class="">Genres</span>
</a>
</router-link>
<router-link tag="li" to="/music/radio" active-class="is-active">
<a>
<span class="icon is-small"><i class="mdi mdi-radio"></i></span>
<span class="">Radio</span>
</a>
</router-link>
<router-link tag="li" to="/music/spotify" v-if="spotify_enabled" active-class="is-active">
<a>
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span>

View File

@ -24,3 +24,16 @@ Vue.filter('timeFromNow', function (value, withoutSuffix) {
Vue.filter('number', function (value) {
return value.toLocaleString()
})
Vue.filter('channels', function (value) {
if (value === 1) {
return 'mono'
}
if (value === 2) {
return 'stereo'
}
if (!value) {
return ''
}
return value + ' channels'
})

View File

@ -7,14 +7,14 @@ import store from './store'
import './filter'
import './progress'
import vClickOutside from 'v-click-outside'
import 'bulma/css/bulma.css'
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img'
import 'mdi/css/materialdesignicons.css'
import 'vue-range-slider/dist/vue-range-slider.css'
import './mystyles.css'
import './mystyles.scss'
Vue.config.productionTip = false
Vue.use(vClickOutside)
Vue.use(VueTinyLazyloadImg)
/* eslint-disable no-new */
new Vue({

View File

@ -1,4 +1,7 @@
@import 'bulma';
.slider {
min-width: 250px;
width: 100%;
@ -100,6 +103,10 @@ section.fd-tabs-section + section.fd-content {
margin-top: 24px;
}
section.hero + section.fd-content {
padding-top: 0;
}
.fd-progress-bar {
top: 52px !important;
}
@ -246,4 +253,43 @@ hr.fd-navbar-divider {
.fd-bottom-navbar .navbar-menu {
max-height: calc(100vh - 3.25rem - 3.25rem - 1rem);
overflow: scroll;
}
.buttons {
@include mobile {
&.fd-is-centered-mobile {
justify-content: center;
&:not(.has-addons) {
.button:not(.is-fullwidth) {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
}
}
}
}
.column {
&.fd-has-cover {
max-height: 150px;
max-width: 150px;
@include mobile {
margin: auto;
}
@include from($tablet) {
margin: auto 0 auto auto;
}
}
}
.fd-overlay-fullscreen {
@extend .is-overlay;
z-index:25;
background-color: rgba(10, 10, 10, 0.2);
position: fixed;
}
.hero-body {
padding: 1.5rem !important;
}

View File

@ -1,21 +1,29 @@
<template>
<content-with-heading>
<content-with-hero>
<template slot="heading-left">
<div class="title is-4">{{ album.name }}</div>
<a class="title is-4 has-text-link has-text-weight-normal" @click="open_artist">{{ album.artist }}</a>
</template>
<template slot="heading-right">
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
</a>
<h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2>
<div class="buttons fd-is-centered-mobile fd-has-margin-top">
<a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
</a>
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
</a>
</div>
</template>
<template slot="heading-right">
<p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="album.artwork_url"
:artist="album.artist"
:album="album.name"
@click="show_album_details_modal = true" />
</p>
</template>
<template slot="content">
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index)">
<template slot="actions">
<a @click="open_dialog(track)">
@ -26,15 +34,16 @@
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
</template>
</content-with-heading>
</content-with-hero>
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import ContentWithHero from '@/templates/ContentWithHero'
import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import CoverArtwork from '@/components/CoverArtwork'
import webapi from '@/webapi'
const albumData = {
@ -54,7 +63,7 @@ const albumData = {
export default {
name: 'PageAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
components: { ContentWithHero, ListItemTrack, ModalDialogTrack, ModalDialogAlbum, CoverArtwork },
data () {
return {

View File

@ -47,7 +47,7 @@ import * as types from '@/store/mutation_types'
const albumsData = {
load: function (to) {
return webapi.library_albums()
return webapi.library_albums('music')
},
set: function (vm, response) {

View File

@ -40,7 +40,7 @@ const artistData = {
load: function (to) {
return Promise.all([
webapi.library_artist(to.params.artist_id),
webapi.library_albums(to.params.artist_id)
webapi.library_artist_albums(to.params.artist_id)
])
},

View File

@ -28,7 +28,7 @@ import webapi from '@/webapi'
const albumsData = {
load: function (to) {
return webapi.library_audiobooks()
return webapi.library_albums('audiobook')
},
set: function (vm, response) {

View File

@ -0,0 +1,106 @@
<template>
<div>
<tabs-music></tabs-music>
<content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left">
<p class="title is-4">Artists</p>
<p class="heading">{{ artists.total }} artists</p>
</template>
<template slot="heading-right">
<a class="button is-small" :class="{ 'is-info': hide_singles }" @click="update_hide_singles">
<span class="icon">
<i class="mdi mdi-numeric-1-box-multiple-outline"></i>
</span>
<span>Hide singles</span>
</a>
</template>
<template slot="content">
<list-item-artist v-for="artist in artists_filtered"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)">
<template slot="actions">
<a @click="open_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-artist>
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" @close="show_details_modal = false" />
</template>
</content-with-heading>
</div>
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemArtist from '@/components/ListItemArtist'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
const artistsData = {
load: function (to) {
return webapi.library_artists()
},
set: function (vm, response) {
vm.artists = response.data
}
}
export default {
name: 'PageArtists',
mixins: [LoadDataBeforeEnterMixin(artistsData)],
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
data () {
return {
artists: { items: [] },
show_details_modal: false,
selected_artist: {}
}
},
computed: {
hide_singles () {
return this.$store.state.hide_singles
},
index_list () {
return [...new Set(this.artists.items
.filter(artist => !this.$store.state.hide_singles || artist.track_count > (artist.album_count * 2))
.map(artist => artist.name_sort.charAt(0).toUpperCase()))]
},
artists_filtered () {
return this.artists.items.filter(artist => !this.hide_singles || artist.track_count > (artist.album_count * 2))
}
},
methods: {
update_hide_singles: function (e) {
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
},
open_artist: function (artist) {
this.$router.push({ path: '/music/artists/' + artist.id })
},
open_dialog: function (artist) {
this.selected_artist = artist
this.show_details_modal = true
}
}
}
</script>
<style>
</style>

View File

@ -101,7 +101,7 @@ import webapi from '@/webapi'
const albumsData = {
load: function (to) {
return Promise.all([
webapi.library_podcasts(),
webapi.library_albums('podcast'),
webapi.library_podcasts_new_episodes()
])
},
@ -195,7 +195,7 @@ export default {
},
reload_podcasts: function () {
webapi.library_podcasts().then(({ data }) => {
webapi.library_albums('podcast').then(({ data }) => {
this.albums = data
this.reload_new_episodes()
})

View File

@ -0,0 +1,70 @@
<template>
<div>
<tabs-music></tabs-music>
<content-with-heading>
<template slot="heading-left">
<p class="title is-4">Radio</p>
</template>
<template slot="content">
<p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p>
<list-item-track v-for="track in tracks.items" :key="track.id" :track="track" @click="play_track(track)">
<template slot="actions">
<a @click="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-track>
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
</template>
</content-with-heading>
</div>
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import TabsMusic from '@/components/TabsMusic'
import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi'
const streamsData = {
load: function (to) {
return webapi.library_radio_streams()
},
set: function (vm, response) {
vm.tracks = response.data.tracks
}
}
export default {
name: 'PageRadioStreams',
mixins: [LoadDataBeforeEnterMixin(streamsData)],
components: { TabsMusic, ContentWithHeading, ListItemTrack, ModalDialogTrack },
data () {
return {
tracks: { items: [] },
show_details_modal: false,
selected_track: {}
}
},
methods: {
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
}
}
}
</script>
<style>
</style>

View File

@ -49,7 +49,7 @@
</label>
</div>
</div>
<form @submit.prevent="kickoff_verification" v-if="output.needs_auth_key" class="fd-has-margin-bottom">
<form @submit.prevent="kickoff_verification(output.id)" v-if="output.needs_auth_key" class="fd-has-margin-bottom">
<div class="field is-grouped">
<div class="control">
<input class="input" type="text" placeholder="Enter verification code" v-model="verification_req.pin">
@ -100,8 +100,8 @@ export default {
webapi.output_toggle(outputId)
},
kickoff_verification () {
webapi.verification_kickoff(this.verification_req)
kickoff_verification (outputId) {
webapi.output_update(outputId, this.verification_req)
}
},

View File

@ -1,21 +1,31 @@
<template>
<content-with-heading>
<content-with-hero>
<template slot="heading-left">
<div class="title is-4">{{ album.name }}</div>
<a class="title is-4 has-text-link has-text-weight-normal" @click="open_artist">{{ album.artists[0].name }}</a>
</template>
<template slot="heading-right">
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
</a>
<h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artists[0].name }}</a></h2>
<div class="buttons fd-is-centered-mobile fd-has-margin-top">
<a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
</a>
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
</a>
</div>
</template>
<template slot="heading-right">
<p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="artwork_url"
:artist="album.artist"
:album="album.name"
@click="show_album_details_modal = true" />
</p>
</template>
<template slot="content">
<p class="heading has-text-centered-mobile">{{ album.tracks.total }} tracks</p>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.tracks.total }} tracks</p>
<spotify-list-item-track v-for="(track, index) in album.tracks.items" :key="track.id" :track="track" :position="index" :album="album" :context_uri="album.uri">
<template slot="actions">
<a @click="open_track_dialog(track)">
@ -26,15 +36,16 @@
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="album" @close="show_track_details_modal = false" />
<spotify-modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
</template>
</content-with-heading>
</content-with-hero>
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import ContentWithHero from '@/templates/ContentWithHero'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import CoverArtwork from '@/components/CoverArtwork'
import store from '@/store'
import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js'
@ -54,7 +65,7 @@ const albumData = {
export default {
name: 'PageAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum },
components: { ContentWithHero, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum, CoverArtwork },
data () {
return {
@ -67,6 +78,15 @@ export default {
}
},
computed: {
artwork_url: function () {
if (this.album.images && this.album.images.length > 0) {
return this.album.images[0].url
}
return ''
}
},
methods: {
open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id })

View File

@ -22,6 +22,7 @@ import PageAudiobook from '@/pages/PageAudiobook'
import PagePlaylists from '@/pages/PagePlaylists'
import PagePlaylist from '@/pages/PagePlaylist'
import PageFiles from '@/pages/PageFiles'
import PageRadioStreams from '@/pages/PageRadioStreams'
import PageSearch from '@/pages/PageSearch'
import PageAbout from '@/pages/PageAbout'
import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse'
@ -125,6 +126,12 @@ export const router = new VueRouter({
component: PageGenreTracks,
meta: { show_progress: true, has_index: true }
},
{
path: '/music/radio',
name: 'Radio',
component: PageRadioStreams,
meta: { show_progress: true, has_tabs: true }
},
{
path: '/podcasts',
name: 'Podcasts',

View File

@ -0,0 +1,44 @@
<template>
<div>
<section class="hero is-light is-bold fd-content">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<div class="columns" style="flex-direction: row-reverse;">
<div class="column fd-has-cover">
<!-- Slot heading right -->
<slot name="heading-right"></slot>
</div>
<div class="column is-three-fifths has-text-centered-mobile" style="margin: auto 0;">
<!-- Slot heading left -->
<slot name="heading-left"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section fd-content">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<!-- Slot content -->
<slot name="content"></slot>
<div style="margin-top: 16px;">
<!-- Slot footer -->
<slot name="footer"></slot>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
</script>
<style>
</style>

View File

@ -208,11 +208,12 @@ export default {
return axios.get('/api/library/artists/' + artistId)
},
library_albums (artistId) {
if (artistId) {
return axios.get('/api/library/artists/' + artistId + '/albums')
}
return axios.get('/api/library/albums?media_kind=music')
library_artist_albums (artistId) {
return axios.get('/api/library/artists/' + artistId + '/albums')
},
library_albums (media_kind = undefined) {
return axios.get('/api/library/albums', { params: { media_kind: media_kind } })
},
library_album (albumId) {
@ -255,6 +256,17 @@ export default {
})
},
library_radio_streams () {
var params = {
type: 'tracks',
media_kind: 'music',
expression: 'data_kind is url and song_length = 0'
}
return axios.get('/api/search', {
params: params
})
},
library_artist_tracks (artist) {
if (artist) {
var artistParams = {
@ -267,10 +279,6 @@ export default {
}
},
library_podcasts () {
return axios.get('/api/library/albums?media_kind=podcast')
},
library_podcasts_new_episodes () {
var episodesParams = {
type: 'tracks',
@ -299,10 +307,6 @@ export default {
return axios.delete('/api/library/playlists/' + playlistId, undefined)
},
library_audiobooks () {
return axios.get('/api/library/albums?media_kind=audiobook')
},
library_playlists () {
return axios.get('/api/library/playlists')
},
@ -372,10 +376,6 @@ export default {
return axios.post('/api/pairing', pairingReq)
},
verification_kickoff (verificationReq) {
return axios.post('/api/verification', verificationReq)
},
artwork_url_append_size_params (artworkUrl, maxwidth = 600, maxheight = 600) {
if (artworkUrl && artworkUrl.startsWith('/')) {
if (artworkUrl.includes('?')) {