mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 15:45:56 -05:00
Merge pull request #1008 from chme/web_next
Player web interface v0.7.2
This commit is contained in:
commit
3fd812ef2f
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
@ -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
7153
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
})
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
])
|
||||
},
|
||||
|
||||
|
@ -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) {
|
||||
|
106
web-src/src/pages/PageAudiobooksArtists.vue
Normal file
106
web-src/src/pages/PageAudiobooksArtists.vue
Normal 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>
|
@ -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()
|
||||
})
|
||||
|
70
web-src/src/pages/PageRadioStreams.vue
Normal file
70
web-src/src/pages/PageRadioStreams.vue
Normal 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>
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -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',
|
||||
|
44
web-src/src/templates/ContentWithHero.vue
Normal file
44
web-src/src/templates/ContentWithHero.vue
Normal 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>
|
@ -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('?')) {
|
||||
|
Loading…
Reference in New Issue
Block a user