Merge pull request #634 from chme/webinterface

Update player webinterface (v0.3.0)
This commit is contained in:
chme 2018-12-24 07:22:37 +01:00 committed by GitHub
commit 4961dd9141
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2715 additions and 1836 deletions

View File

@ -1,2 +1,2 @@
.fd-notifications{position:fixed;bottom:60px;z-index:20000;width:100%}.fd-notifications .notification{margin-bottom:10px;margin-left:24px;margin-right:24px;-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.slider{min-width:250px;width:100%}.range-slider-fill{background-color:#363636}a.navbar-item{outline:0;line-height:1.5;padding:.5rem 1rem}.fd-expanded{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1}.fd-margin-left-auto{margin-left:auto}.fd-has-action{cursor:pointer}.fd-is-movable{cursor:move}.fd-has-margin-top{margin-top:24px}.fd-has-margin-bottom{margin-bottom:24px}.fd-has-padding-left-right{padding-left:24px;padding-right:24px}.fd-is-text-clipped{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fd-is-fullheight{height:calc(100vh - 6.5rem)}.fd-is-fullheight-body{-ms-flex-negative:1;flex-shrink:1;overflow:hidden;height:100%}.fd-image-fullheight{height:100%;width:auto}.fd-tabs-section{padding-bottom:0}.fd-progress-bar{top:52px!important}.fd-has-shadow{-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.sortable-chosen .media-right{visibility:hidden}.sortable-ghost h1,.sortable-ghost h2{color:#ff3860!important}.media:first-of-type{padding-top:17px;margin-top:16px}.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .4s;transition:opacity .4s}.fade-enter,.fade-leave-to{opacity:0}.seek-slider{min-width:250px;max-width:500px;width:100%!important}.seek-slider .range-slider-fill{background-color:#00d1b2;-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.seek-slider .range-slider-knob{width:10px;height:10px;background-color:#00d1b2;border-color:#00d1b2}.title:not(.is-spaced)+.subtitle,.title:not(.is-spaced)+.subtitle+.subtitle{margin-top:-1.3rem!important}.fd-modal-card{overflow:visible}.fd-modal-card .card-content{max-height:calc(100vh - 200px);overflow:auto} .fd-notifications{position:fixed;bottom:60px;z-index:20000;width:100%}.fd-notifications .notification{margin-bottom:10px;margin-left:24px;margin-right:24px;-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.slider{min-width:250px;width:100%}.range-slider-fill{background-color:#363636}a.navbar-item{outline:0;line-height:1.5;padding:.5rem 1rem}.fd-expanded{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1}.fd-margin-left-auto{margin-left:auto}.fd-has-action{cursor:pointer}.fd-is-movable{cursor:move}.fd-has-margin-top{margin-top:24px}.fd-has-margin-bottom{margin-bottom:24px}.fd-remove-padding-bottom{padding-bottom:0}.fd-has-padding-left-right{padding-left:24px;padding-right:24px}.fd-is-square .button{height:27px;width:27px}.fd-is-text-clipped{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fd-is-fullheight{height:calc(100vh - 6.5rem)}.fd-is-fullheight-body{-ms-flex-negative:1;flex-shrink:1;overflow:hidden;height:100%}.fd-image-fullheight{height:100%;width:auto}.fd-tabs-section{padding-bottom:3px;padding-top:3px;background:#fff;top:3.25rem;z-index:20;position:fixed;width:100%}section.fd-tabs-section+section.fd-content{margin-top:24px}.fd-progress-bar{top:52px!important}.fd-has-shadow{-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.sortable-chosen .media-right{visibility:hidden}.sortable-ghost h1,.sortable-ghost h2{color:#ff3860!important}.media:first-of-type{padding-top:17px;margin-top:16px}.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .4s;transition:opacity .4s}.fade-enter,.fade-leave-to{opacity:0}.seek-slider{min-width:250px;max-width:500px;width:100%!important}.seek-slider .range-slider-fill{background-color:#00d1b2;-webkit-box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.seek-slider .range-slider-knob{width:10px;height:10px;background-color:#00d1b2;border-color:#00d1b2}.title:not(.is-spaced)+.subtitle,.title:not(.is-spaced)+.subtitle+.subtitle{margin-top:-1.3rem!important}.fd-modal-card{overflow:visible}.fd-modal-card .card-content{max-height:calc(100vh - 200px);overflow:auto}.fd-modal-card .card{margin-left:16px;margin-right:16px}
/*# sourceMappingURL=app.css.map */ /*# sourceMappingURL=app.css.map */

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

1490
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", "name": "forked-daapd-web",
"version": "0.2.0", "version": "0.3.0",
"description": "forked-daapd web interface", "description": "forked-daapd web interface",
"author": "chme <christian.meffert@googlemail.com>", "author": "chme <christian.meffert@googlemail.com>",
"license": "GPL-2.0", "license": "GPL-2.0",
@ -15,24 +15,24 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"bulma": "^0.7.1", "bulma": "^0.7.1",
"mdi": "^2.1.99", "mdi": "^2.1.99",
"moment": "^2.22.1", "moment": "^2.23.0",
"moment-duration-format": "^2.2.2", "moment-duration-format": "^2.2.2",
"npm": "^6.4.1", "npm": "^6.5.0",
"reconnectingwebsocket": "^1.0.0", "reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.1.1", "spotify-web-api-js": "^1.1.1",
"vue": "^2.5.17", "vue": "^2.5.21",
"vue-infinite-loading": "^2.4.0", "vue-infinite-loading": "^2.4.3",
"vue-progressbar": "^0.7.4", "vue-progressbar": "^0.7.4",
"vue-range-slider": "^0.6.0", "vue-range-slider": "^0.6.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.2",
"vuedraggable": "^2.16.0", "vuedraggable": "^2.17.0",
"vuex": "^3.0.1" "vuex": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.1.1", "@vue/cli-plugin-babel": "^3.2.0",
"@vue/cli-plugin-eslint": "^3.1.4", "@vue/cli-plugin-eslint": "^3.2.1",
"@vue/cli-service": "^3.1.2", "@vue/cli-service": "^3.2.0",
"@vue/eslint-config-standard": "^3.0.5", "@vue/eslint-config-standard": "^3.0.5",
"vue-template-compiler": "^2.5.17" "vue-template-compiler": "^2.5.21"
} }
} }

View File

@ -165,13 +165,11 @@ export default {
this.$store.commit(types.UPDATE_SPOTIFY, data) this.$store.commit(types.UPDATE_SPOTIFY, data)
if (this.token_timer_id > 0) { if (this.token_timer_id > 0) {
console.log('clear old timer: ' + this.token_timer_id)
window.clearTimeout(this.token_timer_id) window.clearTimeout(this.token_timer_id)
this.token_timer_id = 0 this.token_timer_id = 0
} }
if (data.webapi_token_expires_in > 0 && data.webapi_token) { if (data.webapi_token_expires_in > 0 && data.webapi_token) {
this.token_timer_id = window.setTimeout(this.update_spotify, 1000 * data.webapi_token_expires_in) this.token_timer_id = window.setTimeout(this.update_spotify, 1000 * data.webapi_token_expires_in)
console.log('new timer: ' + this.token_timer_id + ', expires in ' + data.webapi_token_expires_in + ' seconds')
} }
}) })
} }

View File

@ -0,0 +1,38 @@
<template>
<section>
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 48px;" v-if="filtered_index.length > 1">
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a>
</nav>
<nav class="buttons is-centered" style="margin-bottom: 6px;" v-if="filtered_index.length > 1">
<a class="button is-small is-white" @click="scroll_to_top"><span class="icon is-small"><i class="mdi mdi-chevron-up"></i></span></a>
</nav>
</section>
</template>
<script>
export default {
name: 'IndexButtonList',
props: ['index'],
computed: {
filtered_index () {
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
return this.index.filter(c => !specialChars.includes(c))
}
},
methods: {
nav: function (id) {
this.$router.push({ path: this.$router.currentRoute.path + '#index_' + id })
},
scroll_to_top: function () {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
}
</script>
<style>
</style>

View File

@ -1,105 +1,19 @@
<template> <template functional>
<div class="media"> <div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" v-on:click="open_album"> <div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ album.name }}</h1> <h1 class="title is-6">{{ props.album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artist }}</b></h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details_modal = true"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<div class="content is-small">
<p v-if="album.artist && media_kind !== 'audiobook'">
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
</p>
<p v-if="album.artist && media_kind === 'audiobook'">
<span class="heading">Album artist</span>
<span class="title is-6">{{ album.artist }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ album.track_count }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi'
export default { export default {
name: 'ListItemAlbum', name: 'ListItemAlbum',
components: { ModalDialog }, props: ['album', 'media_kind']
props: ['album', 'media_kind'],
data () {
return {
show_details_modal: false
}
},
methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.album.uri, false)
},
queue_add: function () {
this.show_details_modal = false
webapi.queue_add(this.album.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
},
queue_add_next: function () {
this.show_details_modal = false
webapi.queue_add_next(this.album.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
},
open_album: function () {
this.show_details_modal = false
if (this.media_kind === 'podcast') {
this.$router.push({ path: '/podcasts/' + this.album.id })
} else if (this.media_kind === 'audiobook') {
this.$router.push({ path: '/audiobooks/' + this.album.id })
} else {
this.$router.push({ path: '/music/albums/' + this.album.id })
}
},
open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/artists/' + this.album.artist_id })
}
}
} }
</script> </script>

View File

@ -1,89 +1,18 @@
<template> <template functional>
<div class="media"> <div class="media" :id="'index_' + props.artist.name_sort.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist"> <div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ artist.name }}</h1> <h1 class="title is-6">{{ props.artist.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details_modal = true"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Albums</span>
<span class="title is-6">{{ artist.album_count }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ artist.track_count }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi'
export default { export default {
name: 'PartArtist', name: 'ListItemArtist',
components: { ModalDialog }, props: ['artist']
props: ['artist'],
data () {
return {
show_details_modal: false
}
},
methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.artist.uri, false)
},
queue_add: function () {
this.show_details_modal = false
webapi.queue_add(this.artist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Artist tracks appended to queue', type: 'info', timeout: 2000 })
)
},
queue_add_next: function () {
this.show_details_modal = false
webapi.queue_add_next(this.artist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
},
open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/artists/' + this.artist.id })
}
}
} }
</script> </script>

View File

@ -1,97 +1,18 @@
<template> <template functional>
<div class="media"> <div class="media" :id="'index_' + props.genre.name.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" v-on:click="open_genre"> <div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ genre.name }}</h1> <h1 class="title is-6">{{ props.genre.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details_modal = true"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_genre">{{ genre.name }}</a>
</p>
<!--
<div class="content is-small">
<p>
<span class="heading">Albums</span>
<span class="title is-6">{{ genre.album_count }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ genre.track_count }}</span>
</p>
</div>
-->
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-12px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-12px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-12px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi'
export default { export default {
name: 'PartGenre', name: 'ListItemGenre',
components: { ModalDialog }, props: [ 'genre' ]
props: [ 'genre' ],
data () {
return {
show_details_modal: false
}
},
methods: {
play: function () {
this.show_details_modal = false
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.player_play_uri(data.albums.items.map(a => a.uri).join(','), false)
)
},
queue_add: function () {
this.show_details_modal = false
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add(data.albums.items.map(a => a.uri).join(',')).then(() =>
this.$store.dispatch('add_notification', { text: 'Genre albums appended to queue', type: 'info', timeout: 1500 })
)
)
},
queue_add_next: function () {
this.show_details_modal = false
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add_next(data.albums.items.map(a => a.uri).join(',')).then(() =>
this.$store.dispatch('add_notification', { text: 'Genre albums playing next', type: 'info', timeout: 1500 })
)
)
},
open_genre: function () {
this.show_details_modal = false
this.$router.push({ name: 'Genre', params: { genre: this.genre.name } })
}
}
} }
</script> </script>

View File

@ -1,85 +1,18 @@
<template> <template functional>
<div class="media"> <div class="media">
<div class="media-content fd-has-action is-clipped" v-on:click="open_playlist"> <div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ playlist.name }}</h1> <h1 class="title is-6">{{ props.playlist.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details_modal = true"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ playlist.path }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi'
export default { export default {
name: 'PartPlaylist', name: 'ListItemPlaylist',
components: { ModalDialog }, props: ['playlist']
props: ['playlist'],
data () {
return {
show_details_modal: false
}
},
methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.playlist.uri, false)
},
queue_add: function () {
this.show_details_modal = false
webapi.queue_add(this.playlist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Playlist appended to queue', type: 'info', timeout: 2000 })
)
},
queue_add_next: function () {
this.show_details_modal = false
webapi.queue_add_next(this.playlist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
},
open_playlist: function () {
this.show_details_modal = false
this.$router.push({ path: '/playlists/' + this.playlist.id })
}
}
} }
</script> </script>

View File

@ -1,6 +1,5 @@
<template> <template>
<div class="media" v-if="is_next || !show_only_next_items"> <div class="media" v-if="is_next || !show_only_next_items">
<!---->
<div class="media-left" v-if="edit_mode"> <div class="media-left" v-if="edit_mode">
<span class="icon has-text-grey fd-is-movable handle"><i class="mdi mdi-drag-horizontal mdi-18px"></i></span> <span class="icon has-text-grey fd-is-movable handle"><i class="mdi mdi-drag-horizontal mdi-18px"></i></span>
</div> </div>
@ -11,84 +10,18 @@
<h2 class="subtitle is-7" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next, 'has-text-grey': is_next && item.id !== state.item_id }">{{ item.album }}</h2> <h2 class="subtitle is-7" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next, 'has-text-grey': is_next && item.id !== state.item_id }">{{ item.album }}</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a v-on:click="remove" v-if="item.id !== state.item_id && edit_mode"> <slot name="actions"></slot>
<span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span>
</a>
<a @click="show_details_modal = true" v-if="!edit_mode">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog v-if="!edit_mode" :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ item.title }}
</p>
<p class="subtitle">
{{ item.artist }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<span class="title is-6">{{ item.album }}</span>
</p>
<p v-if="item.album_artist">
<span class="heading">Album artist</span>
<span class="title is-6">{{ item.album_artist }}</span>
</p>
<p v-if="item.year > 0">
<span class="heading">Year</span>
<span class="title is-6">{{ item.year }}</span>
</p>
<p>
<span class="heading">Genre</span>
<span class="title is-6">{{ item.genre }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ item.track_number }} / {{ item.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ item.length_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ item.path }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="remove">
<span class="icon"><i class="mdi mdi-delete mdi-18px"></i></span> <span>Remove</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'PartQueueItem', name: 'ListItemQueueItem',
components: { ModalDialog },
props: ['item', 'position', 'current_position', 'show_only_next_items', 'edit_mode'], props: ['item', 'position', 'current_position', 'show_only_next_items', 'edit_mode'],
data () {
return {
show_details_modal: false
}
},
computed: { computed: {
state () { state () {
return this.$store.state.player return this.$store.state.player
@ -100,13 +33,7 @@ export default {
}, },
methods: { methods: {
remove: function () {
this.show_details_modal = false
webapi.queue_remove(this.item.id)
},
play: function () { play: function () {
this.show_details_modal = false
webapi.player_play({ 'item_id': this.item.id }) webapi.player_play({ 'item_id': this.item.id })
} }
} }

View File

@ -1,142 +1,20 @@
<template> <template functional>
<div class="media"> <div class="media" :id="'index_' + props.track.title_sort.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" v-on:click="play"> <div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ track.title }}</h1> <h1 class="title is-6">{{ props.track.title }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ track.artist }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ props.track.artist }}</b></h2>
<h2 class="subtitle is-7 has-text-grey">{{ track.album }}</h2> <h2 class="subtitle is-7 has-text-grey">{{ props.track.album }}</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details_modal = true"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
<template slot="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ track.title }}
</p>
<p class="subtitle">
{{ track.artist }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ track.album }}</a>
</p>
<p v-if="track.album_artist && track.media_kind !== 'audiobook'">
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ track.album_artist }}</a>
</p>
<p v-if="track.date_released">
<span class="heading">Release date</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>
<span class="title is-6">{{ track.year }}</span>
</p>
<p>
<span class="heading">Genre</span>
<span class="title is-6">{{ track.genre }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ track.length_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ track.path }}</span>
</p>
<p>
<span class="heading">Type</span>
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }}</span>
</p>
<p>
<span class="heading">Added at</span>
<span class="title is-6">{{ track.time_added | time('L LT')}}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play_track">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</template>
</modal-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog'
import webapi from '@/webapi'
export default { export default {
name: 'PartTrack', name: 'ListItemTrack',
components: { ModalDialog }, props: ['track']
props: ['track', 'position', 'context_uri'],
data () {
return {
show_details_modal: false
}
},
methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.context_uri, false, this.position)
},
play_track: function () {
this.show_details_modal = false
webapi.player_play_uri(this.track.uri, false)
},
queue_add: function () {
this.show_details_modal = false
webapi.queue_add(this.track.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Track appended to queue', type: 'info', timeout: 2000 })
)
},
queue_add_next: function () {
this.show_details_modal = false
webapi.queue_add_next(this.track.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
},
open_album: function () {
this.show_details_modal = false
if (this.track.media_kind === 'podcast') {
this.$router.push({ path: '/podcasts/' + this.track.album_id })
} else if (this.track.media_kind === 'audiobook') {
this.$router.push({ path: '/audiobooks/' + this.track.album_id })
} else {
this.$router.push({ path: '/music/albums/' + this.track.album_id })
}
},
open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/artists/' + this.track.album_artist_id })
}
}
} }
</script> </script>

View File

@ -0,0 +1,113 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<figure class="image is-square fd-has-margin-bottom" v-show="artwork_visible">
<img :src="artwork_url" @load="artwork_loaded" @error="artwork_error" class="fd-has-shadow">
</figure>
<p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<div class="content is-small">
<p v-if="album.artist && media_kind !== 'audiobook'">
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
</p>
<p v-if="album.artist && media_kind === 'audiobook'">
<span class="heading">Album artist</span>
<span class="title is-6">{{ album.artist }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ album.track_count }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogAlbum',
props: [ 'show', 'album', 'media_kind' ],
data () {
return {
artwork_visible: false
}
},
computed: {
artwork_url: function () {
if (this.album.artwork_url && this.album.artwork_url.startsWith('/')) {
return this.album.artwork_url + '?maxwidth=600&maxheight=600'
}
return this.album.artwork_url
}
},
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.album.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.album.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.album.uri)
},
open_album: function () {
if (this.media_kind === 'podcast') {
this.$router.push({ path: '/podcasts/' + this.album.id })
} else if (this.media_kind === 'audiobook') {
this.$router.push({ path: '/audiobooks/' + this.album.id })
} else {
this.$router.push({ path: '/music/albums/' + this.album.id })
}
},
open_artist: function () {
this.$router.push({ path: '/music/artists/' + this.album.artist_id })
},
artwork_loaded: function () {
this.artwork_visible = true
},
artwork_error: function () {
this.artwork_visible = false
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,74 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Albums</span>
<span class="title is-6">{{ artist.album_count }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ artist.track_count }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogArtist',
props: [ 'show', 'artist' ],
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.artist.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.artist.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.artist.uri)
},
open_artist: function () {
this.$emit('close')
this.$router.push({ path: '/music/artists/' + this.artist.id })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,70 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_genre">{{ genre.name }}</a>
</p>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogGenre',
props: [ 'show', 'genre' ],
methods: {
play: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.player_play_uri(data.albums.items.map(a => a.uri).join(','), false)
)
},
queue_add: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add(data.albums.items.map(a => a.uri).join(','))
)
},
queue_add_next: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add_next(data.albums.items.map(a => a.uri).join(','))
)
},
open_genre: function () {
this.$emit('close')
this.$router.push({ name: 'Genre', params: { genre: this.genre.name } })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,70 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ playlist.path }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylist',
props: [ 'show', 'playlist' ],
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.playlist.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.playlist.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.playlist.uri)
},
open_playlist: function () {
this.$emit('close')
this.$router.push({ path: '/playlists/' + this.playlist.id })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,88 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ item.title }}
</p>
<p class="subtitle">
{{ item.artist }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<span class="title is-6">{{ item.album }}</span>
</p>
<p v-if="item.album_artist">
<span class="heading">Album artist</span>
<span class="title is-6">{{ item.album_artist }}</span>
</p>
<p v-if="item.composer">
<span class="heading">Composer</span>
<span class="title is-6">{{ item.composer }}</span>
</p>
<p v-if="item.year > 0">
<span class="heading">Year</span>
<span class="title is-6">{{ item.year }}</span>
</p>
<p>
<span class="heading">Genre</span>
<span class="title is-6">{{ item.genre }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ item.track_number }} / {{ item.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ item.length_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ item.path }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="remove">
<span class="icon"><i class="mdi mdi-delete"></i></span> <span class="is-size-7">Remove</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogQueueItem',
props: [ 'show', 'item' ],
methods: {
remove: function () {
this.$emit('close')
webapi.queue_remove(this.item.id)
},
play: function () {
this.$emit('close')
webapi.player_play({ 'item_id': this.item.id })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,134 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ track.title }}
</p>
<p class="subtitle">
{{ track.artist }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ track.album }}</a>
</p>
<p v-if="track.album_artist && track.media_kind !== 'audiobook'">
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ track.album_artist }}</a>
</p>
<p v-if="track.composer">
<span class="heading">Composer</span>
<span class="title is-6">{{ track.composer }}</span>
</p>
<p v-if="track.date_released">
<span class="heading">Release date</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>
<span class="title is-6">{{ track.year }}</span>
</p>
<p>
<span class="heading">Genre</span>
<span class="title is-6">{{ track.genre }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ track.length_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ track.path }}</span>
</p>
<p>
<span class="heading">Type</span>
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }}</span>
</p>
<p>
<span class="heading">Added at</span>
<span class="title is-6">{{ track.time_added | time('L LT') }}</span>
</p>
<p>
<span class="heading">Rating</span>
<span class="title is-6">{{ Math.floor(track.rating / 10) }} / 10</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play_track">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogTrack',
props: ['show', 'track'],
data () {
return {
}
},
methods: {
play_track: function () {
this.$emit('close')
webapi.player_play_uri(this.track.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.track.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.track.uri)
},
open_album: function () {
this.$emit('close')
if (this.track.media_kind === 'podcast') {
this.$router.push({ path: '/podcasts/' + this.track.album_id })
} else if (this.track.media_kind === 'audiobook') {
this.$router.push({ path: '/audiobooks/' + this.track.album_id })
} else {
this.$router.push({ path: '/music/albums/' + this.track.album_id })
}
},
open_artist: function () {
this.$emit('close')
this.$router.push({ path: '/music/artists/' + this.track.album_artist_id })
}
}
}
</script>
<style>
</style>

View File

@ -5,103 +5,20 @@
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<transition name="fade">
<div class="modal is-active" v-if="show_details_modal">
<div class="modal-background" @click="hide_details"></div>
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a>
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
</p>
<p>
<span class="heading">Type</span>
<span class="title is-6">{{ album.album_type }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="hide_details"></button>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import webapi from '@/webapi'
export default { export default {
name: 'SpotifyListItemAlbum', name: 'SpotifyListItemAlbum',
props: ['album'], props: ['album'],
data () {
return {
show_details_modal: false
}
},
methods: { methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.album.uri, false)
},
queue_add: function () {
webapi.queue_add(this.album.uri).then(
// this.$store.commit(types.ADD_NOTIFICATION, { text: 'Album tracks appended to queue', timeout: 0 })
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 3000 })
)
this.show_details_modal = false
},
queue_add_next: function () {
webapi.queue_add_next(this.album.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
show_details: function () {
this.show_details_modal = true
},
hide_details: function () {
this.show_details_modal = false
},
open_album: function () { open_album: function () {
this.$router.push({ path: '/music/spotify/albums/' + this.album.id }) this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
},
open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id })
} }
} }
} }

View File

@ -4,93 +4,18 @@
<h1 class="title is-6">{{ artist.name }}</h1> <h1 class="title is-6">{{ artist.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<transition name="fade">
<div class="modal is-active" v-if="show_details_modal">
<div class="modal-background" @click="hide_details"></div>
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Popularity / Followers</span>
<span class="title is-6">{{ artist.popularity }} / {{ artist.followers.total }}</span>
</p>
<p>
<span class="heading">Genres</span>
<span class="title is-6">{{ artist.genres.join(', ') }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="hide_details"></button>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import webapi from '@/webapi'
export default { export default {
name: 'SpotifyListItemArtist', name: 'SpotifyListItemArtist',
props: ['artist'], props: ['artist'],
data () {
return {
show_details_modal: false
}
},
methods: { methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.artist.uri, false)
},
queue_add: function () {
webapi.queue_add(this.artist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Artist tracks appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
queue_add_next: function () {
webapi.queue_add_next(this.artist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Artist tracks appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
show_details: function () {
this.show_details_modal = true
},
hide_details: function () {
this.show_details_modal = false
},
open_artist: function () { open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/spotify/artists/' + this.artist.id }) this.$router.push({ path: '/music/spotify/artists/' + this.artist.id })
} }
} }

View File

@ -5,97 +5,18 @@
<h2 class="subtitle is-7">{{ playlist.owner.display_name }}</h2> <h2 class="subtitle is-7">{{ playlist.owner.display_name }}</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<transition name="fade">
<div class="modal is-active" v-if="show_details_modal">
<div class="modal-background" @click="hide_details"></div>
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Owner</span>
<span class="title is-6">{{ playlist.owner.display_name }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ playlist.tracks.total }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ playlist.uri }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="hide_details"></button>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import webapi from '@/webapi'
export default { export default {
name: 'SpotifyListItemPlaylist', name: 'SpotifyListItemPlaylist',
props: ['playlist'], props: ['playlist'],
data () {
return {
show_details_modal: false
}
},
methods: { methods: {
play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.playlist.uri, false)
},
queue_add: function () {
webapi.queue_add(this.playlist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Playlist appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
queue_add_next: function () {
webapi.queue_add_next(this.playlist.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Playlist tracks appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
show_details: function () {
this.show_details_modal = true
},
hide_details: function () {
this.show_details_modal = false
},
open_playlist: function () { open_playlist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id }) this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id })
} }
} }

View File

@ -5,64 +5,7 @@
<h2 class="subtitle is-7 has-text-grey"><b>{{ track.artists[0].name }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ track.artists[0].name }}</b></h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click="show_details"> <slot name="actions"></slot>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<transition name="fade">
<div class="modal is-active" v-if="show_details_modal">
<div class="modal-background" @click="hide_details"></div>
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ track.name }}
</p>
<p class="subtitle">
{{ track.artists[0].name }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<p>
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a>
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ track.duration_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ track.uri }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="hide_details"></button>
</div>
</transition>
</div> </div>
</div> </div>
</template> </template>
@ -75,48 +18,9 @@ export default {
props: ['track', 'position', 'album', 'context_uri'], props: ['track', 'position', 'album', 'context_uri'],
data () {
return {
show_details_modal: false
}
},
methods: { methods: {
play: function () { play: function () {
this.show_details_modal = false
webapi.player_play_uri(this.context_uri, false, this.position) webapi.player_play_uri(this.context_uri, false, this.position)
},
queue_add: function () {
webapi.queue_add(this.track.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Track appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
queue_add_next: function () {
webapi.queue_add_next(this.track.uri).then(() =>
this.$store.dispatch('add_notification', { text: 'Track appended to queue', type: 'info', timeout: 2000 })
)
this.show_details_modal = false
},
show_details: function () {
this.show_details_modal = true
},
hide_details: function () {
this.show_details_modal = false
},
open_album: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
},
open_artist: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id })
} }
} }
} }

View File

@ -0,0 +1,107 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<figure class="image is-square fd-has-margin-bottom" v-show="artwork_visible">
<img :src="artwork_url" @load="artwork_loaded" @error="artwork_error" class="fd-has-shadow">
</figure>
<p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a>
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
</p>
<p>
<span class="heading">Type</span>
<span class="title is-6">{{ album.album_type }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'SpotifyModalDialogAlbum',
props: [ 'show', 'album' ],
data () {
return {
artwork_visible: false
}
},
computed: {
artwork_url: function () {
if (this.album.images && this.album.images.length > 0) {
return this.album.images[0].url
}
return ''
}
},
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.album.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.album.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.album.uri)
},
open_album: function () {
this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
},
open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id })
},
artwork_loaded: function () {
this.artwork_visible = true
},
artwork_error: function () {
this.artwork_visible = false
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,73 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Popularity / Followers</span>
<span class="title is-6">{{ artist.popularity }} / {{ artist.followers.total }}</span>
</p>
<p>
<span class="heading">Genres</span>
<span class="title is-6">{{ artist.genres.join(', ') }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'SpotifyModalDialogArtist',
props: [ 'show', 'artist' ],
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.artist.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.artist.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.artist.uri)
},
open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.artist.id })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
</p>
<div class="content is-small">
<p>
<span class="heading">Owner</span>
<span class="title is-6">{{ playlist.owner.display_name }}</span>
</p>
<p>
<span class="heading">Tracks</span>
<span class="title is-6">{{ playlist.tracks.total }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ playlist.uri }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'SpotifyModalDialogPlaylist',
props: [ 'show', 'playlist' ],
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.playlist.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.playlist.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.playlist.uri)
},
open_playlist: function () {
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,96 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ track.name }}
</p>
<p class="subtitle">
{{ track.artists[0].name }}
</p>
<div class="content is-small">
<p>
<span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ album.name }}</a>
</p>
<p>
<span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a>
</p>
<p>
<span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date }}</span>
</p>
<p>
<span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span>
</p>
<p>
<span class="heading">Length</span>
<span class="title is-6">{{ track.duration_ms | duration }}</span>
</p>
<p>
<span class="heading">Path</span>
<span class="title is-6">{{ track.uri }}</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'SpotifyModalDialogTrack',
props: [ 'show', 'track', 'album' ],
methods: {
play: function () {
this.$emit('close')
webapi.player_play_uri(this.track.uri, false)
},
queue_add: function () {
this.$emit('close')
webapi.queue_add(this.track.uri)
},
queue_add_next: function () {
this.$emit('close')
webapi.queue_add_next(this.track.uri)
},
open_album: function () {
this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
},
open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id })
}
}
}
</script>
<style>
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<section class="section fd-tabs-section" v-if="spotify_enabled"> <section class="section fd-remove-padding-bottom" v-if="spotify_enabled">
<div class="container"> <div class="container">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">

View File

@ -38,11 +38,20 @@ a.navbar-item {
margin-bottom: 24px; margin-bottom: 24px;
} }
.fd-remove-padding-bottom {
padding-bottom: 0;
}
.fd-has-padding-left-right { .fd-has-padding-left-right {
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
} }
.fd-is-square .button {
height: 27px;
width: 27px;
}
.fd-is-text-clipped { .fd-is-text-clipped {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -65,7 +74,17 @@ a.navbar-item {
} }
.fd-tabs-section { .fd-tabs-section {
padding-bottom: 0; padding-bottom: 3px;
padding-top: 3px;
background: white;
top: 3.25rem;
z-index: 20;
position: fixed;
width: 100%;
}
section.fd-tabs-section + section.fd-content {
margin-top: 24px;
} }
.fd-progress-bar { .fd-progress-bar {
@ -131,3 +150,7 @@ a.navbar-item {
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
overflow: auto; overflow: auto;
} }
.fd-modal-card .card {
margin-left: 16px;
margin-right: 16px;
}

View File

@ -18,7 +18,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p> <p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" :position="index" :context_uri="album.uri"></list-item-track> <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)">
<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> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -27,6 +34,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const albumData = {
@ -46,12 +54,15 @@ const albumData = {
export default { export default {
name: 'PageAlbum', name: 'PageAlbum',
mixins: [ LoadDataBeforeEnterMixin(albumData) ], mixins: [ LoadDataBeforeEnterMixin(albumData) ],
components: { ContentWithHeading, ListItemTrack }, components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
data () { data () {
return { return {
album: {}, album: {},
tracks: [] tracks: [],
show_details_modal: false,
selected_track: {}
} }
}, },
@ -63,6 +74,15 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
},
play_track: function (position) {
webapi.player_play_uri(this.album.uri, false, position)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
} }
} }
} }

View File

@ -3,6 +3,9 @@
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left"> <template slot="heading-left">
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
<p class="heading">{{ albums.total }} albums</p> <p class="heading">{{ albums.total }} albums</p>
@ -16,7 +19,18 @@
</a> </a>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" v-if="!hide_singles || album.track_count > 2"></list-item-album> <list-item-album v-for="album in albums.items"
:key="album.id"
:album="album"
@click="open_album(album)"
v-if="!hide_singles || album.track_count > 2">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -26,7 +40,9 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
@ -37,17 +53,24 @@ const albumsData = {
set: function (vm, response) { set: function (vm, response) {
vm.albums = response.data vm.albums = response.data
vm.index_list = [...new Set(vm.albums.items
.filter(album => !vm.$store.state.hide_singles || album.track_count > 2)
.map(album => album.name_sort.charAt(0).toUpperCase()))]
} }
} }
export default { export default {
name: 'PageAlbums', name: 'PageAlbums',
mixins: [ LoadDataBeforeEnterMixin(albumsData) ], mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
components: { ContentWithHeading, TabsMusic, ListItemAlbum }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbum, ModalDialogAlbum },
data () { data () {
return { return {
albums: {} albums: { items: [] },
index_list: [],
show_details_modal: false,
selected_album: {}
} }
}, },
@ -60,6 +83,23 @@ export default {
methods: { methods: {
update_hide_singles: function (e) { update_hide_singles: function (e) {
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles) this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
}
},
watch: {
'hide_singles' () {
this.index_list = [...new Set(this.albums.items
.filter(album => !this.$store.state.hide_singles || album.track_count > 2)
.map(album => album.name_sort.charAt(0).toUpperCase()))]
} }
} }
} }

View File

@ -10,7 +10,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p> <p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
<list-item-album v-for="album in albums.items" :key="album.id" :album="album"></list-item-album> <list-item-album v-for="album in albums.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -19,6 +26,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const artistData = { const artistData = {
@ -38,12 +46,15 @@ const artistData = {
export default { export default {
name: 'PageArtist', name: 'PageArtist',
mixins: [ LoadDataBeforeEnterMixin(artistData) ], mixins: [ LoadDataBeforeEnterMixin(artistData) ],
components: { ContentWithHeading, ListItemAlbum }, components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
data () { data () {
return { return {
artist: {}, artist: {},
albums: {} albums: {},
show_details_modal: false,
selected_album: {}
} }
}, },
@ -54,6 +65,15 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true) webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true)
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -1,6 +1,9 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left"> <template slot="heading-left">
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">{{ artist.name }}</p>
</template> </template>
@ -11,7 +14,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p> <p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p>
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" :position="index" :context_uri="track.uri"></list-item-track> <list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" @click="play_track(index)">
<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> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -20,7 +30,9 @@
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const tracksData = { const tracksData = {
@ -38,14 +50,24 @@ const tracksData = {
} }
export default { export default {
name: 'PageTracks', name: 'PageArtistTracks',
mixins: [ LoadDataBeforeEnterMixin(tracksData) ], mixins: [ LoadDataBeforeEnterMixin(tracksData) ],
components: { ContentWithHeading, ListItemTrack }, components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack },
data () { data () {
return { return {
artist: {}, artist: {},
tracks: {} tracks: { items: [] },
show_details_modal: false,
selected_track: {}
}
},
computed: {
index_list () {
return [...new Set(this.tracks.items
.map(track => track.title_sort.charAt(0).toUpperCase()))]
} }
}, },
@ -57,6 +79,15 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true) webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
},
play_track: function (position) {
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), false, position)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
} }
} }
} }

View File

@ -3,6 +3,9 @@
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left"> <template slot="heading-left">
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
<p class="heading">{{ artists.total }} artists</p> <p class="heading">{{ artists.total }} artists</p>
@ -16,7 +19,18 @@
</a> </a>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist" v-if="!hide_singles || artist.track_count > (artist.album_count * 2)"></list-item-artist> <list-item-artist v-for="artist in artists.items"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)"
v-if="!hide_singles || artist.track_count > (artist.album_count * 2)">
<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> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -26,7 +40,9 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemArtist from '@/components/ListItemArtist' import ListItemArtist from '@/components/ListItemArtist'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
@ -43,23 +59,41 @@ const artistsData = {
export default { export default {
name: 'PageArtists', name: 'PageArtists',
mixins: [ LoadDataBeforeEnterMixin(artistsData) ], mixins: [ LoadDataBeforeEnterMixin(artistsData) ],
components: { ContentWithHeading, TabsMusic, ListItemArtist }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
data () { data () {
return { return {
artists: {} artists: { items: [] },
show_details_modal: false,
selected_artist: {}
} }
}, },
computed: { computed: {
hide_singles () { hide_singles () {
return this.$store.state.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()))]
} }
}, },
methods: { methods: {
update_hide_singles: function (e) { update_hide_singles: function (e) {
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles) 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
} }
} }
} }

View File

@ -14,7 +14,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p> <p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" :position="index" :context_uri="album.uri"></list-item-track> <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)">
<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> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -23,6 +30,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const albumData = {
@ -42,18 +50,30 @@ const albumData = {
export default { export default {
name: 'PageAudiobook', name: 'PageAudiobook',
mixins: [ LoadDataBeforeEnterMixin(albumData) ], mixins: [ LoadDataBeforeEnterMixin(albumData) ],
components: { ContentWithHeading, ListItemTrack }, components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
data () { data () {
return { return {
album: {}, album: {},
tracks: [] tracks: [],
show_details_modal: false,
selected_track: {}
} }
}, },
methods: { methods: {
play: function () { play: function () {
webapi.player_play_uri(this.album.uri, false) webapi.player_play_uri(this.album.uri, false)
},
play_track: function (position) {
webapi.player_play_uri(this.album.uri, false, position)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
} }
} }
} }

View File

@ -6,7 +6,14 @@
<p class="heading">{{ albums.total }} audiobooks</p> <p class="heading">{{ albums.total }} audiobooks</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'audiobook'"></list-item-album> <list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'audiobook'" @click="open_album(album)">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_details_modal" :album="selected_album" :media_kind="'audiobook'" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -16,6 +23,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumsData = { const albumsData = {
@ -31,11 +39,25 @@ const albumsData = {
export default { export default {
name: 'PageAudiobooks', name: 'PageAudiobooks',
mixins: [ LoadDataBeforeEnterMixin(albumsData) ], mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
components: { ContentWithHeading, ListItemAlbum }, components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
data () { data () {
return { return {
albums: {} albums: {},
show_details_modal: false,
selected_album: {}
}
},
methods: {
open_album: function (album) {
this.$router.push({ path: '/audiobooks/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -9,7 +9,14 @@
<p class="heading">albums</p> <p class="heading">albums</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in recently_added.items" :key="album.id" :album="album"></list-item-album> <list-item-album v-for="album in recently_added.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions">
<a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav class="level"> <nav class="level">
@ -27,7 +34,14 @@
<p class="heading">tracks</p> <p class="heading">tracks</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" :position="0" :context_uri="track.uri"></list-item-track> <list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" @click="play_track(track)">
<template slot="actions">
<a @click="open_track_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_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav class="level"> <nav class="level">
@ -46,6 +60,8 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const browseData = { const browseData = {
@ -65,18 +81,42 @@ const browseData = {
export default { export default {
name: 'PageBrowse', name: 'PageBrowse',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ListItemTrack }, components: { ContentWithHeading, TabsMusic, ListItemAlbum, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
data () { data () {
return { return {
recently_added: {}, recently_added: {},
recently_played: {} recently_played: {},
show_track_details_modal: false,
selected_track: {},
show_album_details_modal: false,
selected_album: {}
} }
}, },
methods: { methods: {
open_browse: function (type) { open_browse: function (type) {
this.$router.push({ path: '/music/browse/' + type }) this.$router.push({ path: '/music/browse/' + type })
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_album_dialog: function (album) {
this.selected_album = album
this.show_album_details_modal = true
},
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
} }
} }
} }

View File

@ -8,7 +8,14 @@
<p class="heading">albums</p> <p class="heading">albums</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in recently_added.items" :key="album.id" :album="album"></list-item-album> <list-item-album v-for="album in recently_added.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -19,6 +26,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const browseData = { const browseData = {
@ -38,11 +46,25 @@ const browseData = {
export default { export default {
name: 'PageBrowseType', name: 'PageBrowseType',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, ListItemAlbum }, components: { ContentWithHeading, TabsMusic, ListItemAlbum, ModalDialogAlbum },
data () { data () {
return { return {
recently_added: {} recently_added: {},
show_details_modal: false,
selected_album: {}
}
},
methods: {
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -8,7 +8,14 @@
<p class="heading">tracks</p> <p class="heading">tracks</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" :position="0" :context_uri="track.uri"></list-item-track> <list-item-track v-for="track in recently_played.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> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -19,6 +26,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const browseData = { const browseData = {
@ -38,11 +46,25 @@ const browseData = {
export default { export default {
name: 'PageBrowseType', name: 'PageBrowseType',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, ListItemTrack }, components: { ContentWithHeading, TabsMusic, ListItemTrack, ModalDialogTrack },
data () { data () {
return { return {
recently_played: {} recently_played: {},
show_details_modal: false,
selected_track: {}
}
},
methods: {
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
},
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
} }
} }
} }

View File

@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left"> <template slot="heading-left">
<p class="title is-4">{{ name }}</p> <p class="title is-4">{{ name }}</p>
<p class="heading">{{ genreAlbums.total }} albums</p>
</template> </template>
<template slot="heading-right"> <template slot="heading-right">
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
@ -13,7 +13,15 @@
</a> </a>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-albums v-for="album in genreAlbums.items" :key="album.id" :album="album" :links="links"></list-item-albums> <p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p>
<list-item-albums v-for="album in genre_albums.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-albums>
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -23,7 +31,9 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemAlbums from '@/components/ListItemAlbum' import ListItemAlbums from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const genreData = { const genreData = {
@ -33,40 +43,49 @@ const genreData = {
set: function (vm, response) { set: function (vm, response) {
vm.name = vm.$route.params.genre vm.name = vm.$route.params.genre
vm.genreAlbums = response.data.albums vm.genre_albums = response.data.albums
var li = 0
var v = null
var i
for (i = 0; i < vm.genreAlbums.items.length; i++) {
var n = vm.genreAlbums.items[i].name_sort.charAt(0).toUpperCase()
if (n !== v) {
var obj = {}
obj.n = n
obj.a = 'idx_nav_' + li
vm.links.push(obj)
li++
v = n
}
}
} }
} }
export default { export default {
name: 'PageGenre', name: 'PageGenre',
mixins: [ LoadDataBeforeEnterMixin(genreData) ], mixins: [ LoadDataBeforeEnterMixin(genreData) ],
components: { ContentWithHeading, TabsMusic, ListItemAlbums }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbums, ModalDialogAlbum },
data () { data () {
return { return {
name: '', name: '',
genreAlbums: {}, genre_albums: { items: [] },
links: []
show_details_modal: false,
selected_album: {}
}
},
computed: {
index_list () {
return [...new Set(this.genre_albums.items
.map(album => album.name.charAt(0).toUpperCase()))]
} }
}, },
methods: { methods: {
open_tracks: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/genres/' + this.name + '/tracks' })
},
play: function () { play: function () {
webapi.player_play_uri(this.genreAlbums.items.map(a => a.uri).join(','), true) webapi.player_play_uri(this.genre_albums.items.map(a => a.uri).join(','), true)
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -0,0 +1,94 @@
<template>
<div>
<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">{{ genre }}</p>
</template>
<template slot="heading-right">
<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>
</template>
<template slot="content">
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p>
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" @click="play_track(index)">
<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 ContentWithHeading from '@/templates/ContentWithHeading'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi'
const tracksData = {
load: function (to) {
return webapi.library_genre_tracks(to.params.genre)
},
set: function (vm, response) {
vm.genre = vm.$route.params.genre
vm.tracks = response.data.tracks
}
}
export default {
name: 'PageGenreTracks',
mixins: [ LoadDataBeforeEnterMixin(tracksData) ],
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack },
data () {
return {
tracks: { items: [] },
genre: '',
show_details_modal: false,
selected_track: {}
}
},
computed: {
index_list () {
return [...new Set(this.tracks.items
.map(track => track.title_sort.charAt(0).toUpperCase()))]
}
},
methods: {
open_genre: function () {
this.show_details_modal = false
this.$router.push({ path: '/music/genres/' + this.genre })
},
play: function () {
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
},
play_track: function (position) {
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), false, position)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
}
}
}
</script>
<style>
</style>

View File

@ -3,12 +3,22 @@
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options">
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left"> <template slot="heading-left">
<p class="title is-4">Genres</p> <p class="title is-4">Genres</p>
<p class="heading">{{ genres.total }} genres</p> <p class="heading">{{ genres.total }} genres</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre"></list-item-genre> <list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre" @click="open_genre(genre)">
<template slot="actions">
<a @click="open_dialog(genre)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-genre>
<modal-dialog-genre :show="show_details_modal" :genre="selected_genre" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -18,7 +28,9 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import IndexButtonList from '@/components/IndexButtonList'
import ListItemGenre from '@/components/ListItemGenre' import ListItemGenre from '@/components/ListItemGenre'
import ModalDialogGenre from '@/components/ModalDialogGenre'
import webapi from '@/webapi' import webapi from '@/webapi'
const genresData = { const genresData = {
@ -34,15 +46,33 @@ const genresData = {
export default { export default {
name: 'PageGenres', name: 'PageGenres',
mixins: [ LoadDataBeforeEnterMixin(genresData) ], mixins: [ LoadDataBeforeEnterMixin(genresData) ],
components: { ContentWithHeading, TabsMusic, ListItemGenre }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre },
data () { data () {
return { return {
genres: {} genres: { items: [] },
show_details_modal: false,
selected_genre: {}
}
},
computed: {
index_list () {
return [...new Set(this.genres.items
.map(genre => genre.name.charAt(0).toUpperCase()))]
} }
}, },
methods: { methods: {
open_genre: function (genre) {
this.$router.push({ name: 'Genre', params: { genre: genre.name } })
},
open_dialog: function (genre) {
this.selected_genre = genre
this.show_details_modal = true
}
} }
} }
</script> </script>

View File

@ -14,7 +14,15 @@
</div> </div>
</div> </div>
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="artwork_visible"> <div class="hero-body fd-is-fullheight-body has-text-centered" v-show="artwork_visible">
<img :src="artwork_url" class="fd-has-shadow fd-image-fullheight" @load="artwork_loaded" @error="artwork_error"> <img :src="artwork_url" class="fd-has-shadow fd-image-fullheight fd-has-action"
@load="artwork_loaded"
@error="artwork_error"
@click="open_dialog(now_playing)">
</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>
<div class="hero-foot fd-has-padding-left-right"> <div class="hero-foot fd-has-padding-left-right">
<div class="container has-text-centered fd-has-margin-bottom"> <div class="container has-text-centered fd-has-margin-bottom">
@ -41,11 +49,13 @@
<player-button-consume class="button is-medium is-light"></player-button-consume> <player-button-consume class="button is-medium is-light"></player-button-consume>
</div> </div>
</div> </div>
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
</div> </div>
</section> </section>
</template> </template>
<script> <script>
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause' import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause'
import PlayerButtonNext from '@/components/PlayerButtonNext' import PlayerButtonNext from '@/components/PlayerButtonNext'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious' import PlayerButtonPrevious from '@/components/PlayerButtonPrevious'
@ -58,13 +68,16 @@ import * as types from '@/store/mutation_types'
export default { export default {
name: 'PageNowPlaying', name: 'PageNowPlaying',
components: { PlayerButtonPlayPause, PlayerButtonNext, PlayerButtonPrevious, PlayerButtonShuffle, PlayerButtonConsume, PlayerButtonRepeat, RangeSlider }, components: { ModalDialogQueueItem, PlayerButtonPlayPause, PlayerButtonNext, PlayerButtonPrevious, PlayerButtonShuffle, PlayerButtonConsume, PlayerButtonRepeat, RangeSlider },
data () { data () {
return { return {
item_progress_ms: 0, item_progress_ms: 0,
interval_id: 0, interval_id: 0,
artwork_visible: false artwork_visible: false,
show_details_modal: false,
selected_item: {}
} }
}, },
@ -118,6 +131,11 @@ export default {
artwork_error: function () { artwork_error: function () {
this.artwork_visible = false this.artwork_visible = false
},
open_dialog: function (item) {
this.selected_item = item
this.show_details_modal = true
} }
}, },

View File

@ -10,7 +10,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p> <p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" :position="index" :context_uri="playlist.uri"></list-item-track> <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)">
<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> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -19,6 +26,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const playlistData = { const playlistData = {
@ -38,18 +46,30 @@ const playlistData = {
export default { export default {
name: 'PagePlaylist', name: 'PagePlaylist',
mixins: [ LoadDataBeforeEnterMixin(playlistData) ], mixins: [ LoadDataBeforeEnterMixin(playlistData) ],
components: { ContentWithHeading, ListItemTrack }, components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
data () { data () {
return { return {
playlist: {}, playlist: {},
tracks: [] tracks: [],
show_details_modal: false,
selected_track: {}
} }
}, },
methods: { methods: {
play: function () { play: function () {
webapi.player_play_uri(this.playlist.uri, true) webapi.player_play_uri(this.playlist.uri, true)
},
play_track: function (position) {
webapi.player_play_uri(this.playlist.uri, false, position)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
} }
} }
} }

View File

@ -5,7 +5,14 @@
<p class="heading">{{ playlists.total }} playlists</p> <p class="heading">{{ playlists.total }} playlists</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist"></list-item-playlist> <list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template slot="actions">
<a @click="open_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-playlist>
<modal-dialog-playlist :show="show_details_modal" :playlist="selected_playlist" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -15,6 +22,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import ListItemPlaylist from '@/components/ListItemPlaylist' import ListItemPlaylist from '@/components/ListItemPlaylist'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
import webapi from '@/webapi' import webapi from '@/webapi'
const playlistsData = { const playlistsData = {
@ -30,11 +38,25 @@ const playlistsData = {
export default { export default {
name: 'PagePlaylists', name: 'PagePlaylists',
mixins: [ LoadDataBeforeEnterMixin(playlistsData) ], mixins: [ LoadDataBeforeEnterMixin(playlistsData) ],
components: { ContentWithHeading, TabsMusic, ListItemPlaylist }, components: { ContentWithHeading, TabsMusic, ListItemPlaylist, ModalDialogPlaylist },
data () { data () {
return { return {
playlists: {} playlists: {},
show_details_modal: false,
selected_playlist: {}
}
},
methods: {
open_playlist: function (playlist) {
this.$router.push({ path: '/playlists/' + playlist.id })
},
open_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_details_modal = true
} }
} }
} }

View File

@ -13,7 +13,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p> <p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" :position="index" :context_uri="album.uri"></list-item-track> <list-item-track v-for="track in tracks" :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> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -22,6 +29,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const albumData = {
@ -41,18 +49,30 @@ const albumData = {
export default { export default {
name: 'PagePodcast', name: 'PagePodcast',
mixins: [ LoadDataBeforeEnterMixin(albumData) ], mixins: [ LoadDataBeforeEnterMixin(albumData) ],
components: { ContentWithHeading, ListItemTrack }, components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
data () { data () {
return { return {
album: {}, album: {},
tracks: [] tracks: [],
show_details_modal: false,
selected_track: {}
} }
}, },
methods: { methods: {
play: function () { play: function () {
webapi.player_play_uri(this.album.uri, false) webapi.player_play_uri(this.album.uri, false)
},
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
} }
} }
} }

View File

@ -6,7 +6,14 @@
<p class="heading">{{ albums.total }} podcasts</p> <p class="heading">{{ albums.total }} podcasts</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'podcast'"></list-item-album> <list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'podcast'" @click="open_album(album)">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_details_modal" :album="selected_album" :media_kind="'podcast'" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -16,6 +23,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumsData = { const albumsData = {
@ -31,11 +39,25 @@ const albumsData = {
export default { export default {
name: 'PagePodcasts', name: 'PagePodcasts',
mixins: [ LoadDataBeforeEnterMixin(albumsData) ], mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
components: { ContentWithHeading, ListItemAlbum }, components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
data () { data () {
return { return {
albums: {} albums: {},
show_details_modal: false,
selected_album: {}
}
},
methods: {
open_album: function (album) {
this.$router.push({ path: '/podcasts/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -40,8 +40,18 @@
:key="item.id" :item="item" :position="index" :key="item.id" :item="item" :position="index"
:current_position="current_position" :current_position="current_position"
:show_only_next_items="show_only_next_items" :show_only_next_items="show_only_next_items"
:edit_mode="edit_mode"></list-item-queue-item> :edit_mode="edit_mode">
<template slot="actions">
<a @click="open_dialog(item)" v-if="!edit_mode">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
<a @click="remove(item)" v-if="item.id !== state.item_id && edit_mode">
<span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span>
</a>
</template>
</list-item-queue-item>
</draggable> </draggable>
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -49,17 +59,21 @@
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemQueueItem from '@/components/ListItemQueueItem' import ListItemQueueItem from '@/components/ListItemQueueItem'
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
export default { export default {
name: 'PageQueue', name: 'PageQueue',
components: { ContentWithHeading, ListItemQueueItem, draggable }, components: { ContentWithHeading, ListItemQueueItem, draggable, ModalDialogQueueItem },
data () { data () {
return { return {
edit_mode: false edit_mode: false,
show_details_modal: false,
selected_item: {}
} }
}, },
@ -92,6 +106,10 @@ export default {
this.$store.commit(types.SHOW_ONLY_NEXT_ITEMS, !this.show_only_next_items) this.$store.commit(types.SHOW_ONLY_NEXT_ITEMS, !this.show_only_next_items)
}, },
remove: function (item) {
webapi.queue_remove(item.id)
},
move_item: function (e) { move_item: function (e) {
var oldPosition = !this.show_only_next_items ? e.oldIndex : e.oldIndex + this.current_position var oldPosition = !this.show_only_next_items ? e.oldIndex : e.oldIndex + this.current_position
var item = this.queue_items[oldPosition] var item = this.queue_items[oldPosition]
@ -99,6 +117,11 @@ export default {
if (newPosition !== oldPosition) { if (newPosition !== oldPosition) {
webapi.queue_move(item.id, newPosition) webapi.queue_move(item.id, newPosition)
} }
},
open_dialog: function (item) {
this.selected_item = item
this.show_details_modal = true
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- Search field + recent searches --> <!-- Search field + recent searches -->
<section class="section fd-tabs-section"> <section class="section fd-remove-padding-bottom">
<div class="container"> <div class="container">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
@ -31,7 +31,14 @@
<p class="title is-4">Tracks</p> <p class="title is-4">Tracks</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-track v-for="track in tracks.items" :key="track.id" :track="track" :position="0" :context_uri="track.uri"></list-item-track> <list-item-track v-for="track in tracks.items" :key="track.id" :track="track" @click="play_track(track)">
<template slot="actions">
<a @click="open_track_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_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_tracks_button" class="level"> <nav v-if="show_all_tracks_button" class="level">
@ -49,7 +56,14 @@
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist"></list-item-artist> <list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist" @click="open_artist(artist)">
<template slot="actions">
<a @click="open_artist_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_artist_details_modal" :artist="selected_artist" @close="show_artist_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_artists_button" class="level"> <nav v-if="show_all_artists_button" class="level">
@ -67,7 +81,14 @@
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-album v-for="album in albums.items" :key="album.id" :album="album"></list-item-album> <list-item-album v-for="album in albums.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions">
<a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-album>
<modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_albums_button" class="level"> <nav v-if="show_all_albums_button" class="level">
@ -85,7 +106,14 @@
<p class="title is-4">Playlists</p> <p class="title is-4">Playlists</p>
</template> </template>
<template slot="content"> <template slot="content">
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist"></list-item-playlist> <list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template slot="actions">
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-playlist>
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_playlists_button" class="level"> <nav v-if="show_all_playlists_button" class="level">
@ -106,12 +134,16 @@ import ListItemTrack from '@/components/ListItemTrack'
import ListItemArtist from '@/components/ListItemArtist' import ListItemArtist from '@/components/ListItemArtist'
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum'
import ListItemPlaylist from '@/components/ListItemPlaylist' import ListItemPlaylist from '@/components/ListItemPlaylist'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
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: 'PageSearch', name: 'PageSearch',
components: { ContentWithHeading, TabsSearch, ListItemTrack, ListItemArtist, ListItemAlbum, ListItemPlaylist }, components: { ContentWithHeading, TabsSearch, ListItemTrack, ListItemArtist, ListItemAlbum, ListItemPlaylist, ModalDialogTrack, ModalDialogAlbum, ModalDialogArtist, ModalDialogPlaylist },
data () { data () {
return { return {
@ -119,7 +151,19 @@ export default {
tracks: { items: [], total: 0 }, tracks: { items: [], total: 0 },
artists: { items: [], total: 0 }, artists: { items: [], total: 0 },
albums: { items: [], total: 0 }, albums: { items: [], total: 0 },
playlists: { items: [], total: 0 } playlists: { items: [], total: 0 },
show_track_details_modal: false,
selected_track: {},
show_album_details_modal: false,
selected_album: {},
show_artist_details_modal: false,
selected_artist: {},
show_playlist_details_modal: false,
selected_playlist: {}
} }
}, },
@ -238,9 +282,45 @@ export default {
}) })
}, },
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
},
open_artist: function (artist) {
this.$router.push({ path: '/music/artists/' + artist.id })
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_playlist: function (playlist) {
this.$router.push({ path: '/playlists/' + playlist.id })
},
open_recent_search: function (query) { open_recent_search: function (query) {
this.search_query = query this.search_query = query
this.new_search() this.new_search()
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
open_album_dialog: function (album) {
this.selected_album = album
this.show_album_details_modal = true
},
open_artist_dialog: function (artist) {
this.selected_artist = artist
this.show_artist_details_modal = true
},
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
} }
}, },

View File

@ -11,7 +11,14 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ album.tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">{{ 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"></spotify-list-item-track> <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)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-track>
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="album" @close="show_track_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -20,6 +27,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack' import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import store from '@/store' import store from '@/store'
import webapi from '@/webapi' import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
@ -39,11 +47,14 @@ const albumData = {
export default { export default {
name: 'PageAlbum', name: 'PageAlbum',
mixins: [ LoadDataBeforeEnterMixin(albumData) ], mixins: [ LoadDataBeforeEnterMixin(albumData) ],
components: { ContentWithHeading, SpotifyListItemTrack }, components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack },
data () { data () {
return { return {
album: {} album: { artists: [{}], tracks: {} },
show_track_details_modal: false,
selected_track: {}
} }
}, },
@ -55,6 +66,11 @@ export default {
play: function () { play: function () {
this.show_details_modal = false this.show_details_modal = false
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
} }
} }
} }

View File

@ -5,8 +5,15 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ total }} albums</p> <p class="heading has-text-centered-mobile">{{ total }} albums</p>
<spotify-list-item-album v-for="album in albums" :key="album.id" :album="album"></spotify-list-item-album> <spotify-list-item-album v-for="album in albums" :key="album.id" :album="album">
<template slot="actions">
<a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-album>
<infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -15,6 +22,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import store from '@/store' import store from '@/store'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import InfiniteLoading from 'vue-infinite-loading' import InfiniteLoading from 'vue-infinite-loading'
@ -42,14 +50,17 @@ const artistData = {
export default { export default {
name: 'SpotifyPageArtist', name: 'SpotifyPageArtist',
mixins: [ LoadDataBeforeEnterMixin(artistData) ], mixins: [ LoadDataBeforeEnterMixin(artistData) ],
components: { ContentWithHeading, SpotifyListItemAlbum, InfiniteLoading }, components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, InfiniteLoading },
data () { data () {
return { return {
artist: {}, artist: {},
albums: [], albums: [],
total: 0, total: 0,
offset: 0 offset: 0,
show_details_modal: false,
selected_album: {}
} }
}, },
@ -73,6 +84,11 @@ export default {
$state.complete() $state.complete()
} }
} }
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
} }
} }
} }

View File

@ -8,7 +8,14 @@
<p class="title is-4">New Releases</p> <p class="title is-4">New Releases</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album"></spotify-list-item-album> <spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album">
<template slot="actions">
<a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-album>
<spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav class="level"> <nav class="level">
@ -27,7 +34,14 @@
<p class="title is-4">Featured Playlists</p> <p class="title is-4">Featured Playlists</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist"></spotify-list-item-playlist> <spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist">
<template slot="actions">
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-playlist>
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav class="level"> <nav class="level">
@ -48,6 +62,8 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
@ -77,7 +93,17 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowse', name: 'SpotifyPageBrowse',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist }, components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist },
data () {
return {
show_album_details_modal: false,
selected_album: {},
show_playlist_details_modal: false,
selected_playlist: {}
}
},
computed: { computed: {
new_releases () { new_releases () {
@ -87,6 +113,18 @@ export default {
featured_playlists () { featured_playlists () {
return this.$store.state.spotify_featured_playlists.slice(0, 3) return this.$store.state.spotify_featured_playlists.slice(0, 3)
} }
},
methods: {
open_album_dialog: function (album) {
this.selected_album = album
this.show_album_details_modal = true
},
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
}
} }
} }
</script> </script>

View File

@ -7,7 +7,14 @@
<p class="title is-4">Featured Playlists</p> <p class="title is-4">Featured Playlists</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist"></spotify-list-item-playlist> <spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist">
<template slot="actions">
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-playlist>
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -18,6 +25,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
@ -43,12 +51,26 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowseFeaturedPlaylists', name: 'SpotifyPageBrowseFeaturedPlaylists',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, SpotifyListItemPlaylist }, components: { ContentWithHeading, TabsMusic, SpotifyListItemPlaylist, SpotifyModalDialogPlaylist },
data () {
return {
show_playlist_details_modal: false,
selected_playlist: {}
}
},
computed: { computed: {
featured_playlists () { featured_playlists () {
return this.$store.state.spotify_featured_playlists return this.$store.state.spotify_featured_playlists
} }
},
methods: {
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
}
} }
} }
</script> </script>

View File

@ -7,7 +7,14 @@
<p class="title is-4">New Releases</p> <p class="title is-4">New Releases</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album"></spotify-list-item-album> <spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album">
<template slot="actions">
<a @click="open_album(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-album>
<spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -18,6 +25,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic' import TabsMusic from '@/components/TabsMusic'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
@ -43,12 +51,26 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowseNewReleases', name: 'SpotifyPageBrowseNewReleases',
mixins: [ LoadDataBeforeEnterMixin(browseData) ], mixins: [ LoadDataBeforeEnterMixin(browseData) ],
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum }, components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum },
data () {
return {
show_album_details_modal: false,
selected_album: {}
}
},
computed: { computed: {
new_releases () { new_releases () {
return this.$store.state.spotify_new_releases return this.$store.state.spotify_new_releases
} }
},
methods: {
open_album: function (album) {
this.selected_album = album
this.show_album_details_modal = true
}
} }
} }
</script> </script>

View File

@ -10,8 +10,15 @@
</template> </template>
<template slot="content"> <template slot="content">
<p class="heading has-text-centered-mobile">{{ playlist.tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">{{ playlist.tracks.total }} tracks</p>
<spotify-list-item-track v-for="(item, index) in tracks" :key="item.track.id" :track="item.track" :album="item.track.album" :position="index" :context_uri="playlist.uri"></spotify-list-item-track> <spotify-list-item-track v-for="(item, index) in tracks" :key="item.track.id" :track="item.track" :album="item.track.album" :position="index" :context_uri="playlist.uri">
<template slot="actions">
<a @click="open_track_dialog(item.track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-track>
<infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -20,6 +27,7 @@
import { LoadDataBeforeEnterMixin } from './mixin' import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack' import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import store from '@/store' import store from '@/store'
import webapi from '@/webapi' import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
@ -47,14 +55,17 @@ const playlistData = {
export default { export default {
name: 'SpotifyPagePlaylist', name: 'SpotifyPagePlaylist',
mixins: [ LoadDataBeforeEnterMixin(playlistData) ], mixins: [ LoadDataBeforeEnterMixin(playlistData) ],
components: { ContentWithHeading, SpotifyListItemTrack, InfiniteLoading }, components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, InfiniteLoading },
data () { data () {
return { return {
playlist: {}, playlist: { tracks: {} },
tracks: [], tracks: [],
total: 0, total: 0,
offset: 0 offset: 0,
show_track_details_modal: false,
selected_track: {}
} }
}, },
@ -83,6 +94,11 @@ export default {
play: function () { play: function () {
this.show_details_modal = false this.show_details_modal = false
webapi.player_play_uri(this.playlist.uri, true) webapi.player_play_uri(this.playlist.uri, true)
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- Search field + recent searches --> <!-- Search field + recent searches -->
<section class="section fd-tabs-section"> <section class="section fd-remove-padding-bottom">
<div class="container"> <div class="container">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
@ -31,8 +31,15 @@
<p class="title is-4">Tracks</p> <p class="title is-4">Tracks</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-track v-for="track in tracks.items" :key="track.id" :track="track" :album="track.album" :position="0" :context_uri="track.uri"></spotify-list-item-track> <spotify-list-item-track v-for="track in tracks.items" :key="track.id" :track="track" :album="track.album" :position="0" :context_uri="track.uri">
<template slot="actions">
<a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-track>
<infinite-loading v-if="query.type === 'track'" @infinite="search_tracks_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="query.type === 'track'" @infinite="search_tracks_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_tracks_button" class="level"> <nav v-if="show_all_tracks_button" class="level">
@ -50,8 +57,15 @@
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist"></spotify-list-item-artist> <spotify-list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist">
<template slot="actions">
<a @click="open_artist_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-artist>
<infinite-loading v-if="query.type === 'artist'" @infinite="search_artists_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="query.type === 'artist'" @infinite="search_artists_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-artist :show="show_artist_details_modal" :artist="selected_artist" @close="show_artist_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_artists_button" class="level"> <nav v-if="show_all_artists_button" class="level">
@ -69,8 +83,15 @@
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-album v-for="album in albums.items" :key="album.id" :album="album"></spotify-list-item-album> <spotify-list-item-album v-for="album in albums.items" :key="album.id" :album="album">
<template slot="actions">
<a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-album>
<infinite-loading v-if="query.type === 'album'" @infinite="search_albums_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="query.type === 'album'" @infinite="search_albums_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_albums_button" class="level"> <nav v-if="show_all_albums_button" class="level">
@ -88,8 +109,15 @@
<p class="title is-4">Playlists</p> <p class="title is-4">Playlists</p>
</template> </template>
<template slot="content"> <template slot="content">
<spotify-list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist"></spotify-list-item-playlist> <spotify-list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist">
<template slot="actions">
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</spotify-list-item-playlist>
<infinite-loading v-if="query.type === 'playlist'" @infinite="search_playlists_next"><span slot="no-more">.</span></infinite-loading> <infinite-loading v-if="query.type === 'playlist'" @infinite="search_playlists_next"><span slot="no-more">.</span></infinite-loading>
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
<template slot="footer"> <template slot="footer">
<nav v-if="show_all_playlists_button" class="level"> <nav v-if="show_all_playlists_button" class="level">
@ -110,6 +138,10 @@ import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyListItemArtist from '@/components/SpotifyListItemArtist' import SpotifyListItemArtist from '@/components/SpotifyListItemArtist'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
@ -117,7 +149,7 @@ import InfiniteLoading from 'vue-infinite-loading'
export default { export default {
name: 'SpotifyPageSearch', name: 'SpotifyPageSearch',
components: { ContentWithHeading, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, InfiniteLoading }, components: { ContentWithHeading, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogTrack, SpotifyModalDialogArtist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, InfiniteLoading },
data () { data () {
return { return {
@ -128,7 +160,19 @@ export default {
playlists: { items: [], total: 0 }, playlists: { items: [], total: 0 },
query: {}, query: {},
search_param: {} search_param: {},
show_track_details_modal: false,
selected_track: {},
show_album_details_modal: false,
selected_album: {},
show_artist_details_modal: false,
selected_artist: {},
show_playlist_details_modal: false,
selected_playlist: {}
} }
}, },
@ -321,6 +365,26 @@ export default {
open_recent_search: function (query) { open_recent_search: function (query) {
this.search_query = query this.search_query = query
this.new_search() this.new_search()
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
open_album_dialog: function (album) {
this.selected_album = album
this.show_album_details_modal = true
},
open_artist_dialog: function (artist) {
this.selected_artist = artist
this.show_artist_details_modal = true
},
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
} }
}, },

View File

@ -13,7 +13,8 @@ import PageAlbums from '@/pages/PageAlbums'
import PageAlbum from '@/pages/PageAlbum' import PageAlbum from '@/pages/PageAlbum'
import PageGenres from '@/pages/PageGenres' import PageGenres from '@/pages/PageGenres'
import PageGenre from '@/pages/PageGenre' import PageGenre from '@/pages/PageGenre'
import PageTracks from '@/pages/PageTracks' import PageGenreTracks from '@/pages/PageGenreTracks'
import PageArtistTracks from '@/pages/PageArtistTracks'
import PagePodcasts from '@/pages/PagePodcasts' import PagePodcasts from '@/pages/PagePodcasts'
import PagePodcast from '@/pages/PagePodcast' import PagePodcast from '@/pages/PagePodcast'
import PageAudiobooks from '@/pages/PageAudiobooks' import PageAudiobooks from '@/pages/PageAudiobooks'
@ -57,25 +58,25 @@ export const router = new VueRouter({
path: '/music/browse', path: '/music/browse',
name: 'Browse', name: 'Browse',
component: PageBrowse, component: PageBrowse,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/browse/recently_added', path: '/music/browse/recently_added',
name: 'Browse Recently Added', name: 'Browse Recently Added',
component: PageBrowseRecentlyAdded, component: PageBrowseRecentlyAdded,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/browse/recently_played', path: '/music/browse/recently_played',
name: 'Browse Recently Played', name: 'Browse Recently Played',
component: PageBrowseRecentlyPlayed, component: PageBrowseRecentlyPlayed,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/artists', path: '/music/artists',
name: 'Artists', name: 'Artists',
component: PageArtists, component: PageArtists,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true, has_index: true }
}, },
{ {
path: '/music/artists/:artist_id', path: '/music/artists/:artist_id',
@ -86,14 +87,14 @@ export const router = new VueRouter({
{ {
path: '/music/artists/:artist_id/tracks', path: '/music/artists/:artist_id/tracks',
name: 'Tracks', name: 'Tracks',
component: PageTracks, component: PageArtistTracks,
meta: { show_progress: true } meta: { show_progress: true, has_index: true }
}, },
{ {
path: '/music/albums', path: '/music/albums',
name: 'Albums', name: 'Albums',
component: PageAlbums, component: PageAlbums,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true, has_index: true }
}, },
{ {
path: '/music/albums/:album_id', path: '/music/albums/:album_id',
@ -105,13 +106,19 @@ export const router = new VueRouter({
path: '/music/genres', path: '/music/genres',
name: 'Genres', name: 'Genres',
component: PageGenres, component: PageGenres,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true, has_index: true }
}, },
{ {
path: '/music/genres/:genre', path: '/music/genres/:genre',
name: 'Genre', name: 'Genre',
component: PageGenre, component: PageGenre,
meta: { show_progress: true } meta: { show_progress: true, has_index: true }
},
{
path: '/music/genres/:genre/tracks',
name: 'GenreTracks',
component: PageGenreTracks,
meta: { show_progress: true, has_index: true }
}, },
{ {
path: '/podcasts', path: '/podcasts',
@ -162,19 +169,19 @@ export const router = new VueRouter({
path: '/music/spotify', path: '/music/spotify',
name: 'Spotify', name: 'Spotify',
component: SpotifyPageBrowse, component: SpotifyPageBrowse,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/spotify/new-releases', path: '/music/spotify/new-releases',
name: 'Spotify Browse New Releases', name: 'Spotify Browse New Releases',
component: SpotifyPageBrowseNewReleases, component: SpotifyPageBrowseNewReleases,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/spotify/featured-playlists', path: '/music/spotify/featured-playlists',
name: 'Spotify Browse Featured Playlists', name: 'Spotify Browse Featured Playlists',
component: SpotifyPageBrowseFeaturedPlaylists, component: SpotifyPageBrowseFeaturedPlaylists,
meta: { show_progress: true } meta: { show_progress: true, has_tabs: true }
}, },
{ {
path: '/music/spotify/artists/:artist_id', path: '/music/spotify/artists/:artist_id',
@ -201,11 +208,30 @@ export const router = new VueRouter({
} }
], ],
scrollBehavior (to, from, savedPosition) { scrollBehavior (to, from, savedPosition) {
// console.log(to.path + '_' + from.path + '__' + to.hash + ' savedPosition:' + savedPosition)
if (savedPosition) { if (savedPosition) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
resolve(savedPosition) resolve(savedPosition)
}, 500) }, 10)
})
} else if (to.path === from.path && to.hash) {
return { selector: to.hash, offset: { x: 0, y: 90 } }
} else if (to.hash) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ selector: to.hash, offset: { x: 0, y: 90 } })
}, 10)
})
} else if (to.meta.has_index) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (to.meta.has_tabs) {
resolve({ selector: '#top', offset: { x: 0, y: 140 } })
} else {
resolve({ selector: '#top', offset: { x: 0, y: 100 } })
}
}, 10)
}) })
} else { } else {
return { x: 0, y: 0 } return { x: 0, y: 0 }

View File

@ -1,9 +1,10 @@
<template> <template>
<section class="section"> <section class="section fd-content">
<div class="container"> <div class="container">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<nav class="level"> <slot name="options"></slot>
<nav class="level" id="top">
<!-- Left side --> <!-- Left side -->
<div class="level-left"> <div class="level-left">
<div class="level-item has-text-centered-mobile"> <div class="level-item has-text-centered-mobile">

View File

@ -41,8 +41,13 @@ export default {
return axios.put('/api/queue/items/' + itemId + '?new_position=' + newPosition) return axios.put('/api/queue/items/' + itemId + '?new_position=' + newPosition)
}, },
queue_add (uri) { queue_add (uri, showNotification = true) {
return axios.post('/api/queue/items/add?uris=' + uri) return axios.post('/api/queue/items/add?uris=' + uri).then((response) => {
if (showNotification) {
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
}
return Promise.resolve(response)
})
}, },
queue_add_next (uri) { queue_add_next (uri) {
@ -50,17 +55,20 @@ export default {
if (store.getters.now_playing && store.getters.now_playing.id) { if (store.getters.now_playing && store.getters.now_playing.id) {
position = store.getters.now_playing.position + 1 position = store.getters.now_playing.position + 1
} }
return axios.post('/api/queue/items/add?uris=' + uri + '&position=' + position) return axios.post('/api/queue/items/add?uris=' + uri + '&position=' + position).then((response) => {
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
return Promise.resolve(response)
})
}, },
player_status () { player_status () {
return axios.get('/api/player') return axios.get('/api/player')
}, },
player_play_uri (uris, shuffle, position = 0) { player_play_uri (uris, shuffle, position = undefined) {
return this.queue_clear().then(() => return this.queue_clear().then(() =>
this.player_shuffle(shuffle).then(() => this.player_shuffle(shuffle).then(() =>
this.queue_add(uris).then(() => this.queue_add(uris, false).then(() =>
this.player_play({ 'position': position }) this.player_play({ 'position': position })
) )
) )
@ -163,6 +171,17 @@ export default {
}) })
}, },
library_genre_tracks (genre) {
var genreParams = {
'type': 'tracks',
'media_kind': 'music',
'expression': 'genre is "' + genre + '"'
}
return axios.get('/api/search', {
params: genreParams
})
},
library_artist_tracks (artist) { library_artist_tracks (artist) {
if (artist) { if (artist) {
var artistParams = { var artistParams = {