[web] Format web sources with prettier and run fix linting errors

This commit is contained in:
chme 2022-02-19 06:39:14 +01:00
parent d7f1c13585
commit c78f861f45
116 changed files with 5274 additions and 2887 deletions

View File

@ -1,13 +1,10 @@
module.exports = { module.exports = {
env: { env: {
node: true, node: true
}, },
extends: [ extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
'eslint:recommended', rules: {
'plugin:vue/vue3-recommended', // override/add rules settings here, such as:
], // 'vue/no-unused-vars': 'error'
rules: { }
// override/add rules settings here, such as: }
// 'vue/no-unused-vars': 'error'
}
}

View File

@ -3,6 +3,7 @@
- Vue Dev Tools required in version 6 (currently only released as beta versions): <https://devtools.vuejs.org/guide/installation.html#beta> - Vue Dev Tools required in version 6 (currently only released as beta versions): <https://devtools.vuejs.org/guide/installation.html#beta>
- [ ] vite does not support env vars in `vite.config.js` from `.env` files - [ ] vite does not support env vars in `vite.config.js` from `.env` files
- <https://stackoverflow.com/questions/66389043/how-can-i-use-vite-env-variables-in-vite-config-js> - <https://stackoverflow.com/questions/66389043/how-can-i-use-vite-env-variables-in-vite-config-js>
- <https://github.com/vitejs/vite/issues/1930> - <https://github.com/vitejs/vite/issues/1930>
@ -13,6 +14,7 @@
- [x] Update dialog is missing scan options - [x] Update dialog is missing scan options
- [ ] Performance with huge artists/albums/tracks list (no functional template supported any more) - [ ] Performance with huge artists/albums/tracks list (no functional template supported any more)
- [ ] Do not reload data, if using the index-nav - [ ] Do not reload data, if using the index-nav
- [x] PageAlbums - [x] PageAlbums
- [ ] PageArtists - [ ] PageArtists
@ -21,54 +23,64 @@
- [ ] Evaluate virtual scroller <https://github.com/Akryum/vue-virtual-scroller/tree/next/packages/vue-virtual-scroller> - [ ] Evaluate virtual scroller <https://github.com/Akryum/vue-virtual-scroller/tree/next/packages/vue-virtual-scroller>
- [x] JS error on Podacst page - [x] JS error on Podacst page
- Problem caused by the Slider component - Problem caused by the Slider component
- Replace with plain html - Replace with plain html
- [ ] vue-router scroll-behavior - [ ] vue-router scroll-behavior
- [x] Index list not always hidden - [x] Index list not always hidden
- [x] Check transitions - [x] Check transitions
- [ ] Page display is "jumpy" - [ ] Page display is "jumpy"
- Workaround is removing the page transition effect - Workaround is removing the page transition effect
- [x] Index navigation "scroll up/down" button does not scroll down, if index is visible - [x] Index navigation "scroll up/down" button does not scroll down, if index is visible
- [x] Use native intersection observer solves it in desktop mode - [x] Use native intersection observer solves it in desktop mode
- [x] Mobile view still broken - [x] Mobile view still broken
- [x] Update to latest dependency versions (vite, vue, etc.) - [x] Update to latest dependency versions (vite, vue, etc.)
- [x] Index navigation is broken (jump to "A") - [x] Index navigation is broken (jump to "A")
- Change in `$router.push` syntax, hash has to be passed as a separate parameter instead of as part of the path - Change in `$router.push` syntax, hash has to be passed as a separate parameter instead of as part of the path
- [x] `vue-range-slider` is not compatible with vue3 - [x] `vue-range-slider` is not compatible with vue3
- replacement option: <https://github.com/vueform/slider> - replacement option: <https://github.com/vueform/slider>
- [x] `@vueform/slider` for volume control - [x] `@vueform/slider` for volume control
- [x] track progress (now playing) - [x] track progress (now playing)
- [x] track progress (podcasts) - [x] track progress (podcasts)
- [x] vue-router does not support navigation guards in mixins: <https://github.com/vuejs/vue-router-next/issues/454> - [x] vue-router does not support navigation guards in mixins: <https://github.com/vuejs/vue-router-next/issues/454>
- replace mixin with composition api? <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards> - replace mixin with composition api? <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards>
- Copied nav guards into each component - Copied nav guards into each component
- [x] vue-router link does not support `tag` and `active-class` properties: <https://next.router.vuejs.org/guide/migration/index.html#removal-of-event-and-tag-props-in-router-link> - [x] vue-router link does not support `tag` and `active-class` properties: <https://next.router.vuejs.org/guide/migration/index.html#removal-of-event-and-tag-props-in-router-link>
- [x] `vue-tiny-lazyload-img` does not support Vue 3 - [x] `vue-tiny-lazyload-img` does not support Vue 3
- No sign of interesst to add support <https://github.com/mazipan/vue-tiny-lazyload-img> - No sign of interesst to add support <https://github.com/mazipan/vue-tiny-lazyload-img>
- `v-lazy-image` (<https://github.com/alexjoverm/v-lazy-image>) seems to be supported and popular - `v-lazy-image` (<https://github.com/alexjoverm/v-lazy-image>) seems to be supported and popular
- Works as a component instead of a directive - Works as a component instead of a directive
- __DOES NOT__ have a good error handling, if the (remote) image does not exist - **DOES NOT** have a good error handling, if the (remote) image does not exist
- `vue3-lazyload` (<https://github.com/murongg/vue3-lazyload>) - `vue3-lazyload` (<https://github.com/murongg/vue3-lazyload>)
- Works as a directive - Works as a directive
- Easy replacement for `vue-tiny-lazyload-img` - Easy replacement for `vue-tiny-lazyload-img`
- [x] Top margin in pages is wrong (related to vue-router scroll behavior changes) - [x] Top margin in pages is wrong (related to vue-router scroll behavior changes)
- Solved by adding the correct margin to take the top navbar (and where shown the tabs) into account - Solved by adding the correct margin to take the top navbar (and where shown the tabs) into account
- [x] Mobile view seems to be broken - [x] Mobile view seems to be broken
- Looks like the cause of this was the broken router-link in bulma tabs component - Looks like the cause of this was the broken router-link in bulma tabs component
- [x] Changing sort option (artist albums view) does not work - [x] Changing sort option (artist albums view) does not work
- [x] Replace unmaintained `vue-infinite-loading` dependency - [x] Replace unmaintained `vue-infinite-loading` dependency
- Replace with `@ts-pro/vue-eternal-loading`: <https://github.com/ts-pro/vue-eternal-loading> - Replace with `@ts-pro/vue-eternal-loading`: <https://github.com/ts-pro/vue-eternal-loading>
- [x] Replace `bulma-switch` with `@vueform/toggle`? - [x] Replace `bulma-switch` with `@vueform/toggle`?

View File

@ -3,13 +3,17 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?ver2.0"> <link
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> sizes="180x180"
<link rel="manifest" href="/site.webmanifest"> href="/apple-touch-icon.png?ver2.0"
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> />
<meta name="msapplication-TileColor" content="#da532c"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<meta name="theme-color" content="#ffffff"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OwnTone</title> <title>OwnTone</title>
</head> </head>

View File

@ -1,19 +1,19 @@
{ {
"name": "OwnTone", "name": "OwnTone",
"short_name": "OwnTone", "short_name": "OwnTone",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/android-chrome-512x512.png", "src": "/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View File

@ -6,14 +6,21 @@
<component :is="Component" class="fd-page" /> <component :is="Component" class="fd-page" />
</router-view> </router-view>
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" /> <modal-dialog-remote-pairing
:show="pairing_active"
@close="pairing_active = false"
/>
<modal-dialog-update <modal-dialog-update
:show="show_update_dialog" :show="show_update_dialog"
@close="show_update_dialog = false" /> @close="show_update_dialog = false"
/>
<notifications v-show="!show_burger_menu" /> <notifications v-show="!show_burger_menu" />
<navbar-bottom /> <navbar-bottom />
<div class="fd-overlay-fullscreen" v-show="show_burger_menu || show_player_menu" <div
@click="show_burger_menu = show_player_menu = false"></div> v-show="show_burger_menu || show_player_menu"
class="fd-overlay-fullscreen"
@click="show_burger_menu = show_player_menu = false"
/>
</div> </div>
</template> </template>
@ -30,10 +37,15 @@ import moment from 'moment'
export default { export default {
name: 'App', name: 'App',
components: { NavbarTop, NavbarBottom, Notifications, ModalDialogRemotePairing, ModalDialogUpdate }, components: {
template: '<App/>', NavbarTop,
NavbarBottom,
Notifications,
ModalDialogRemotePairing,
ModalDialogUpdate
},
data () { data() {
return { return {
token_timer_id: 0, token_timer_id: 0,
reconnect_attempts: 0, reconnect_attempts: 0,
@ -43,31 +55,40 @@ export default {
computed: { computed: {
show_burger_menu: { show_burger_menu: {
get () { get() {
return this.$store.state.show_burger_menu return this.$store.state.show_burger_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_BURGER_MENU, value) this.$store.commit(types.SHOW_BURGER_MENU, value)
} }
}, },
show_player_menu: { show_player_menu: {
get () { get() {
return this.$store.state.show_player_menu return this.$store.state.show_player_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_PLAYER_MENU, value) this.$store.commit(types.SHOW_PLAYER_MENU, value)
} }
}, },
show_update_dialog: { show_update_dialog: {
get () { get() {
return this.$store.state.show_update_dialog return this.$store.state.show_update_dialog
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_UPDATE_DIALOG, value) this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
} }
} }
}, },
watch: {
show_burger_menu() {
this.update_is_clipped()
},
show_player_menu() {
this.update_is_clipped()
}
},
created: function () { created: function () {
moment.locale(navigator.language) moment.locale(navigator.language)
this.connect() this.connect()
@ -97,23 +118,38 @@ export default {
methods: { methods: {
connect: function () { connect: function () {
this.$store.dispatch('add_notification', { text: 'Connecting to OwnTone server', type: 'info', topic: 'connection', timeout: 2000 }) this.$store.dispatch('add_notification', {
text: 'Connecting to OwnTone server',
webapi.config().then(({ data }) => { type: 'info',
this.$store.commit(types.UPDATE_CONFIG, data) topic: 'connection',
this.$store.commit(types.HIDE_SINGLES, data.hide_singles) timeout: 2000
document.title = data.library_name
this.open_ws()
this.$Progress.finish()
}).catch(() => {
this.$store.dispatch('add_notification', { text: 'Failed to connect to OwnTone server', type: 'danger', topic: 'connection' })
}) })
webapi
.config()
.then(({ data }) => {
this.$store.commit(types.UPDATE_CONFIG, data)
this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
document.title = data.library_name
this.open_ws()
this.$Progress.finish()
})
.catch(() => {
this.$store.dispatch('add_notification', {
text: 'Failed to connect to OwnTone server',
type: 'danger',
topic: 'connection'
})
})
}, },
open_ws: function () { open_ws: function () {
if (this.$store.state.config.websocket_port <= 0) { if (this.$store.state.config.websocket_port <= 0) {
this.$store.dispatch('add_notification', { text: 'Missing websocket port', type: 'danger' }) this.$store.dispatch('add_notification', {
text: 'Missing websocket port',
type: 'danger'
})
return return
} }
@ -124,22 +160,47 @@ export default {
protocol = 'wss://' protocol = 'wss://'
} }
let wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port let wsUrl =
if (import.meta.env.NODE_ENV === 'development' && import.meta.env.VUE_APP_WEBSOCKET_SERVER) { protocol +
window.location.hostname +
':' +
vm.$store.state.config.websocket_port
if (
import.meta.env.NODE_ENV === 'development' &&
import.meta.env.VUE_APP_WEBSOCKET_SERVER
) {
// If we are running in the development server, use the websocket url configured in .env.development // If we are running in the development server, use the websocket url configured in .env.development
wsUrl = import.meta.env.VUE_APP_WEBSOCKET_SERVER wsUrl = import.meta.env.VUE_APP_WEBSOCKET_SERVER
} }
const socket = new ReconnectingWebSocket( const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
wsUrl, reconnectInterval: 3000
'notify', })
{ reconnectInterval: 3000 }
)
socket.onopen = function () { socket.onopen = function () {
vm.$store.dispatch('add_notification', { text: 'Connection to server established', type: 'primary', topic: 'connection', timeout: 2000 }) vm.$store.dispatch('add_notification', {
text: 'Connection to server established',
type: 'primary',
topic: 'connection',
timeout: 2000
})
vm.reconnect_attempts = 0 vm.reconnect_attempts = 0
socket.send(JSON.stringify({ notify: ['update', 'database', 'player', 'options', 'outputs', 'volume', 'queue', 'spotify', 'lastfm', 'pairing'] })) socket.send(
JSON.stringify({
notify: [
'update',
'database',
'player',
'options',
'outputs',
'volume',
'queue',
'spotify',
'lastfm',
'pairing'
]
})
)
vm.update_outputs() vm.update_outputs()
vm.update_player_status() vm.update_player_status()
@ -155,14 +216,26 @@ export default {
} }
socket.onerror = function () { socket.onerror = function () {
vm.reconnect_attempts++ vm.reconnect_attempts++
vm.$store.dispatch('add_notification', { text: 'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')', type: 'danger', topic: 'connection' }) vm.$store.dispatch('add_notification', {
text:
'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')',
type: 'danger',
topic: 'connection'
})
} }
socket.onmessage = function (response) { socket.onmessage = function (response) {
const data = JSON.parse(response.data) const data = JSON.parse(response.data)
if (data.notify.includes('update') || data.notify.includes('database')) { if (
data.notify.includes('update') ||
data.notify.includes('database')
) {
vm.update_library_stats() vm.update_library_stats()
} }
if (data.notify.includes('player') || data.notify.includes('options') || data.notify.includes('volume')) { if (
data.notify.includes('player') ||
data.notify.includes('options') ||
data.notify.includes('volume')
) {
vm.update_player_status() vm.update_player_status()
} }
if (data.notify.includes('outputs') || data.notify.includes('volume')) { if (data.notify.includes('outputs') || data.notify.includes('volume')) {
@ -237,7 +310,10 @@ export default {
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
)
} }
}) })
}, },
@ -257,17 +333,8 @@ export default {
} }
} }
}, },
template: '<App/>'
watch: {
'show_burger_menu' () {
this.update_is_clipped()
},
'show_player_menu' () {
this.update_is_clipped()
}
}
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -9,7 +9,7 @@ export default {
_gain: null, _gain: null,
// setup audio routing // setup audio routing
setupAudio () { setupAudio() {
const AudioContext = window.AudioContext || window.webkitAudioContext const AudioContext = window.AudioContext || window.webkitAudioContext
this._context = new AudioContext() this._context = new AudioContext()
this._source = this._context.createMediaElementSource(this._audio) this._source = this._context.createMediaElementSource(this._audio)
@ -18,26 +18,26 @@ export default {
this._source.connect(this._gain) this._source.connect(this._gain)
this._gain.connect(this._context.destination) this._gain.connect(this._context.destination)
this._audio.addEventListener('canplaythrough', e => { this._audio.addEventListener('canplaythrough', (e) => {
this._audio.play() this._audio.play()
}) })
this._audio.addEventListener('canplay', e => { this._audio.addEventListener('canplay', (e) => {
this._audio.play() this._audio.play()
}) })
return this._audio return this._audio
}, },
// set audio volume // set audio volume
setVolume (volume) { setVolume(volume) {
if (!this._gain) return if (!this._gain) return
volume = parseFloat(volume) || 0.0 volume = parseFloat(volume) || 0.0
volume = (volume < 0) ? 0 : volume volume = volume < 0 ? 0 : volume
volume = (volume > 1) ? 1 : volume volume = volume > 1 ? 1 : volume
this._gain.gain.value = volume this._gain.gain.value = volume
}, },
// play audio source url // play audio source url
playSource (source) { playSource(source) {
this.stopAudio() this.stopAudio()
this._context.resume().then(() => { this._context.resume().then(() => {
this._audio.src = String(source || '') + '?x=' + Date.now() this._audio.src = String(source || '') + '?x=' + Date.now()
@ -47,9 +47,15 @@ export default {
}, },
// stop playing audio // stop playing audio
stopAudio () { stopAudio() {
try { this._audio.pause() } catch (e) {} try {
try { this._audio.stop() } catch (e) {} this._audio.pause()
try { this._audio.close() } catch (e) {} } catch (e) {}
try {
this._audio.stop()
} catch (e) {}
try {
this._audio.close()
} catch (e) {}
} }
} }

View File

@ -1,7 +1,9 @@
<template> <template>
<figure> <figure>
<img v-lazy="{ src: artwork_url_with_size, lifecycle: lazy_lifecycle }" <img
@click="$emit('click')"> v-lazy="{ src: artwork_url_with_size, lifecycle: lazy_lifecycle }"
@click="$emit('click')"
/>
</figure> </figure>
</template> </template>
@ -13,7 +15,7 @@ export default {
name: 'CoverArtwork', name: 'CoverArtwork',
props: ['artist', 'album', 'artwork_url', 'maxwidth', 'maxheight'], props: ['artist', 'album', 'artwork_url', 'maxwidth', 'maxheight'],
data () { data() {
return { return {
width: 600, width: 600,
height: 600, height: 600,
@ -31,16 +33,20 @@ export default {
computed: { computed: {
artwork_url_with_size: function () { artwork_url_with_size: function () {
if (this.maxwidth > 0 && this.maxheight > 0) { if (this.maxwidth > 0 && this.maxheight > 0) {
return webapi.artwork_url_append_size_params(this.artwork_url, this.maxwidth, this.maxheight) return webapi.artwork_url_append_size_params(
this.artwork_url,
this.maxwidth,
this.maxheight
)
} }
return webapi.artwork_url_append_size_params(this.artwork_url) return webapi.artwork_url_append_size_params(this.artwork_url)
}, },
alt_text () { alt_text() {
return this.artist + ' - ' + this.album return this.artist + ' - ' + this.album
}, },
caption () { caption() {
if (this.album) { if (this.album) {
return this.album.substring(0, 2) return this.album.substring(0, 2)
} }

View File

@ -1,19 +1,31 @@
<template> <template>
<div class="dropdown" :class="{ 'is-active': is_active }" v-click-away="onClickOutside"> <div
v-click-away="onClickOutside"
class="dropdown"
:class="{ 'is-active': is_active }"
>
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" @click="is_active = !is_active"> <button
class="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
@click="is_active = !is_active"
>
<span>{{ modelValue }}</span> <span>{{ modelValue }}</span>
<span class="icon is-small"> <span class="icon is-small">
<i class="mdi mdi-chevron-down" aria-hidden="true"></i> <i class="mdi mdi-chevron-down" aria-hidden="true" />
</span> </span>
</button> </button>
</div> </div>
<div class="dropdown-menu" id="dropdown-menu" role="menu"> <div id="dropdown-menu" class="dropdown-menu" role="menu">
<div class="dropdown-content"> <div class="dropdown-content">
<a class="dropdown-item" <a
v-for="option in options" :key="option" v-for="option in options"
:class="{'is-active': modelValue === option}" :key="option"
@click="select(option)"> class="dropdown-item"
:class="{ 'is-active': modelValue === option }"
@click="select(option)"
>
{{ option }} {{ option }}
</a> </a>
</div> </div>
@ -28,18 +40,18 @@ export default {
props: ['modelValue', 'options'], props: ['modelValue', 'options'],
emits: ['update:modelValue'], emits: ['update:modelValue'],
data () { data() {
return { return {
is_active: false is_active: false
} }
}, },
methods: { methods: {
onClickOutside (event) { onClickOutside(event) {
this.is_active = false this.is_active = false
}, },
select (option) { select(option) {
this.is_active = false this.is_active = false
this.$emit('update:modelValue', option) this.$emit('update:modelValue', option)
} }
@ -47,5 +59,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,7 +1,13 @@
<template> <template>
<section> <section>
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 16px;"> <nav class="buttons is-centered fd-is-square" style="margin-bottom: 16px">
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a> <a
v-for="char in filtered_index"
:key="char"
class="button is-small"
@click="nav(char)"
>{{ char }}</a
>
</nav> </nav>
</section> </section>
</template> </template>
@ -13,9 +19,9 @@ export default {
props: ['index'], props: ['index'],
computed: { computed: {
filtered_index () { filtered_index() {
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~' const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
return this.index.filter(c => !specialChars.includes(c)) return this.index.filter((c) => !specialChars.includes(c))
} }
}, },
@ -31,5 +37,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,84 +2,105 @@
<div> <div>
<div v-if="is_grouped"> <div v-if="is_grouped">
<div v-for="idx in albums.indexList" :key="idx" class="mb-6"> <div v-for="idx in albums.indexList" :key="idx" class="mb-6">
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span> <span
:id="'index_' + idx"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>
<div class="media" v-for="album in albums.grouped[idx]" <div
:key="album.id" v-for="album in albums.grouped[idx]"
:album="album" :key="album.id"
@click="open_album(album)"> class="media"
<div class="media-left fd-has-action" :album="album"
v-if="is_visible_artwork"> @click="open_album(album)"
<p class="image is-64x64 fd-has-shadow fd-has-action"> >
<cover-artwork <div v-if="is_visible_artwork" class="media-left fd-has-action">
<p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
:maxwidth="64" :maxwidth="64"
:maxheight="64" /> :maxheight="64"
/>
</p> </p>
</div> </div>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<div style="margin-top:0.7rem;"> <div style="margin-top: 0.7rem">
<h1 class="title is-6">{{ album.name }}</h1> <h1 class="title is-6">
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2> {{ album.name }}
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal" </h1>
v-if="album.date_released && album.media_kind === 'music'"> <h2 class="subtitle is-7 has-text-grey">
{{ $filters.time(album.date_released, 'L') }} <b>{{ album.artist }}</b>
</h2> </h2>
</div> <h2
</div> v-if="album.date_released && album.media_kind === 'music'"
<div class="media-right" style="padding-top:0.7rem;"> class="subtitle is-7 has-text-grey has-text-weight-normal"
<a @click.prevent.stop="open_dialog(album)"> >
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> {{ $filters.time(album.date_released, 'L') }}
</h2>
</div>
</div>
<div class="media-right" style="padding-top: 0.7rem">
<a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<list-item-album v-for="album in albums_list" <list-item-album
:key="album.id" v-for="album in albums_list"
:album="album" :key="album.id"
@click="open_album(album)"> :album="album"
<template v-slot:artwork v-if="is_visible_artwork"> @click="open_album(album)"
>
<template v-if="is_visible_artwork" #artwork>
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
:maxwidth="64" :maxwidth="64"
:maxheight="64" /> :maxheight="64"
/>
</p> </p>
</template> </template>
<template v-slot:actions> <template #actions>
<a @click.prevent.stop="open_dialog(album)"> <a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-album> </list-item-album>
</div> </div>
<modal-dialog-album <modal-dialog-album
:show="show_details_modal" :show="show_details_modal"
:album="selected_album" :album="selected_album"
:media_kind="media_kind" :media_kind="media_kind"
@remove-podcast="open_remove_podcast_dialog()" @remove-podcast="open_remove_podcast_dialog()"
@play-count-changed="play_count_changed()" @play-count-changed="play_count_changed()"
@close="show_details_modal = false" /> @close="show_details_modal = false"
/>
<modal-dialog <modal-dialog
:show="show_remove_podcast_modal" :show="show_remove_podcast_modal"
title="Remove podcast" title="Remove podcast"
delete_action="Remove" delete_action="Remove"
@close="show_remove_podcast_modal = false" @close="show_remove_podcast_modal = false"
@delete="remove_podcast"> @delete="remove_podcast"
<template v-slot:modal-content> >
<template #modal-content>
<p>Permanently remove this podcast from your library?</p> <p>Permanently remove this podcast from your library?</p>
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p> <p class="is-size-7">
(This will also remove the RSS playlist
<b>{{ rss_playlist_to_remove.name }}</b
>.)
</p>
</template> </template>
</modal-dialog> </modal-dialog>
</div> </div>
@ -99,7 +120,7 @@ export default {
props: ['albums', 'media_kind'], props: ['albums', 'media_kind'],
data () { data() {
return { return {
show_details_modal: false, show_details_modal: false,
selected_album: {}, selected_album: {},
@ -110,8 +131,11 @@ export default {
}, },
computed: { computed: {
is_visible_artwork () { is_visible_artwork() {
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value return this.$store.getters.settings_option(
'webinterface',
'show_cover_artwork_in_album_lists'
).value
}, },
media_kind_resolved: function () { media_kind_resolved: function () {
@ -129,7 +153,7 @@ export default {
}, },
is_grouped: function () { is_grouped: function () {
return (this.albums instanceof Albums && this.albums.options.group) return this.albums instanceof Albums && this.albums.options.group
} }
}, },
@ -151,19 +175,24 @@ export default {
}, },
open_remove_podcast_dialog: function () { open_remove_podcast_dialog: function () {
webapi.library_album_tracks(this.selected_album.id, { limit: 1 }).then(({ data }) => { webapi
webapi.library_track_playlists(data.items[0].id).then(({ data }) => { .library_album_tracks(this.selected_album.id, { limit: 1 })
const rssPlaylists = data.items.filter(pl => pl.type === 'rss') .then(({ data }) => {
if (rssPlaylists.length !== 1) { webapi.library_track_playlists(data.items[0].id).then(({ data }) => {
this.$store.dispatch('add_notification', { text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.', type: 'danger' }) const rssPlaylists = data.items.filter((pl) => pl.type === 'rss')
return if (rssPlaylists.length !== 1) {
} this.$store.dispatch('add_notification', {
text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.',
type: 'danger'
})
return
}
this.rss_playlist_to_remove = rssPlaylists[0] this.rss_playlist_to_remove = rssPlaylists[0]
this.show_remove_podcast_modal = true this.show_remove_podcast_modal = true
this.show_details_modal = false this.show_details_modal = false
})
}) })
})
}, },
play_count_changed: function () { play_count_changed: function () {
@ -172,13 +201,14 @@ export default {
remove_podcast: function () { remove_podcast: function () {
this.show_remove_podcast_modal = false this.show_remove_podcast_modal = false
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => { webapi
this.$emit('podcast-deleted') .library_playlist_delete(this.rss_playlist_to_remove.id)
}) .then(() => {
this.$emit('podcast-deleted')
})
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,32 +2,49 @@
<div> <div>
<div v-if="is_grouped"> <div v-if="is_grouped">
<div v-for="idx in artists.indexList" :key="idx" class="mb-6"> <div v-for="idx in artists.indexList" :key="idx" class="mb-6">
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span> <span
<list-item-artist v-for="artist in artists.grouped[idx]" :id="'index_' + idx"
:key="artist.id" class="tag is-info is-light is-small has-text-weight-bold"
:artist="artist" >{{ idx }}</span
@click="open_artist(artist)"> >
<template v-slot:actions> <list-item-artist
<a @click.prevent.stop="open_dialog(artist)"> v-for="artist in artists.grouped[idx]"
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> :key="artist.id"
</a> :artist="artist"
</template> @click="open_artist(artist)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist> </list-item-artist>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<list-item-artist v-for="artist in artists_list" <list-item-artist
:key="artist.id" v-for="artist in artists_list"
:artist="artist" :key="artist.id"
@click="open_artist(artist)"> :artist="artist"
<template v-slot:actions> @click="open_artist(artist)"
<a @click.prevent.stop="open_dialog(artist)"> >
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <template #actions>
</a> <a @click.prevent.stop="open_dialog(artist)">
</template> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist> </list-item-artist>
</div> </div>
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" :media_kind="media_kind" @close="show_details_modal = false" /> <modal-dialog-artist
:show="show_details_modal"
:artist="selected_artist"
:media_kind="media_kind"
@close="show_details_modal = false"
/>
</div> </div>
</template> </template>
@ -42,7 +59,7 @@ export default {
props: ['artists', 'media_kind'], props: ['artists', 'media_kind'],
data () { data() {
return { return {
show_details_modal: false, show_details_modal: false,
selected_artist: {} selected_artist: {}
@ -62,7 +79,7 @@ export default {
}, },
is_grouped: function () { is_grouped: function () {
return (this.artists instanceof Artists && this.artists.options.group) return this.artists instanceof Artists && this.artists.options.group
} }
}, },
@ -86,5 +103,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,32 +2,49 @@
<div> <div>
<div v-if="is_grouped"> <div v-if="is_grouped">
<div v-for="idx in composers.indexList" :key="idx" class="mb-6"> <div v-for="idx in composers.indexList" :key="idx" class="mb-6">
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span> <span
<list-item-composer v-for="composer in composers.grouped[idx]" :id="'index_' + idx"
:key="composer.id" class="tag is-info is-light is-small has-text-weight-bold"
:composer="composer" >{{ idx }}</span
@click="open_composer(composer)"> >
<template v-slot:actions> <list-item-composer
<a @click.prevent.stop="open_dialog(composer)"> v-for="composer in composers.grouped[idx]"
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> :key="composer.id"
</a> :composer="composer"
</template> @click="open_composer(composer)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer> </list-item-composer>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<list-item-composer v-for="composer in composers_list" <list-item-composer
:key="composer.id" v-for="composer in composers_list"
:composer="composer" :key="composer.id"
@click="open_composer(composer)"> :composer="composer"
<template v-slot:actions> @click="open_composer(composer)"
<a @click.prevent.stop="open_dialog(composer)"> >
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <template #actions>
</a> <a @click.prevent.stop="open_dialog(composer)">
</template> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer> </list-item-composer>
</div> </div>
<modal-dialog-composer :show="show_details_modal" :composer="selected_composer" :media_kind="media_kind" @close="show_details_modal = false" /> <modal-dialog-composer
:show="show_details_modal"
:composer="selected_composer"
:media_kind="media_kind"
@close="show_details_modal = false"
/>
</div> </div>
</template> </template>
@ -42,7 +59,7 @@ export default {
props: ['composers', 'media_kind'], props: ['composers', 'media_kind'],
data () { data() {
return { return {
show_details_modal: false, show_details_modal: false,
selected_composer: {} selected_composer: {}
@ -51,7 +68,9 @@ export default {
computed: { computed: {
media_kind_resolved: function () { media_kind_resolved: function () {
return this.media_kind ? this.media_kind : this.selected_composer.media_kind return this.media_kind
? this.media_kind
: this.selected_composer.media_kind
}, },
composers_list: function () { composers_list: function () {
@ -62,14 +81,17 @@ export default {
}, },
is_grouped: function () { is_grouped: function () {
return (this.composers instanceof Composers && this.composers.options.group) return this.composers instanceof Composers && this.composers.options.group
} }
}, },
methods: { methods: {
open_composer: function (composer) { open_composer: function (composer) {
this.selected_composer = composer this.selected_composer = composer
this.$router.push({ name: 'ComposerTracks', params: { composer: composer.name } }) this.$router.push({
name: 'ComposerTracks',
params: { composer: composer.name }
})
}, },
open_dialog: function (composer) { open_dialog: function (composer) {
@ -80,5 +102,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,21 +1,26 @@
<template> <template>
<div class="media" :id="'index_' + album.name_sort.charAt(0).toUpperCase()"> <div :id="'index_' + album.name_sort.charAt(0).toUpperCase()" class="media">
<div class="media-left fd-has-action" <div v-if="$slots['artwork']" class="media-left fd-has-action">
v-if="$slots['artwork']"> <slot name="artwork" />
<slot name="artwork"></slot>
</div> </div>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<div style="margin-top:0.7rem;"> <div style="margin-top: 0.7rem">
<h1 class="title is-6">{{ album.name }}</h1> <h1 class="title is-6">
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2> {{ album.name }}
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal" </h1>
v-if="album.date_released && album.media_kind === 'music'"> <h2 class="subtitle is-7 has-text-grey">
<b>{{ album.artist }}</b>
</h2>
<h2
v-if="album.date_released && album.media_kind === 'music'"
class="subtitle is-7 has-text-grey has-text-weight-normal"
>
{{ $filters.time(album.date_released, 'L') }} {{ $filters.time(album.date_released, 'L') }}
</h2> </h2>
</div> </div>
</div> </div>
<div class="media-right" style="padding-top:0.7rem;"> <div class="media-right" style="padding-top: 0.7rem">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -27,5 +32,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,10 +1,12 @@
<template> <template>
<div class="media"> <div class="media">
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<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">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -16,5 +18,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,10 +1,12 @@
<template> <template>
<div class="media" :id="'index_' + composer.name.charAt(0).toUpperCase()"> <div :id="'index_' + composer.name.charAt(0).toUpperCase()" class="media">
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ composer.name }}</h1> <h1 class="title is-6">
{{ composer.name }}
</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -16,5 +18,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,15 +2,19 @@
<div class="media"> <div class="media">
<figure class="media-left fd-has-action"> <figure class="media-left fd-has-action">
<span class="icon"> <span class="icon">
<i class="mdi mdi-folder"></i> <i class="mdi mdi-folder" />
</span> </span>
</figure> </figure>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}</h1> <h1 class="title is-6">
<h2 class="subtitle is-7 has-text-grey-light">{{ directory.path }}</h2> {{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}
</h1>
<h2 class="subtitle is-7 has-text-grey-light">
{{ directory.path }}
</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -22,5 +26,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,10 +1,12 @@
<template> <template>
<div class="media" :id="'index_' + genre.name.charAt(0).toUpperCase()"> <div :id="'index_' + genre.name.charAt(0).toUpperCase()" class="media">
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ genre.name }}</h1> <h1 class="title is-6">
{{ genre.name }}
</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -16,5 +18,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,13 +1,15 @@
<template> <template>
<div class="media"> <div class="media">
<figure class="media-left fd-has-action" v-if="$slots.icon"> <figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon"></slot> <slot name="icon" />
</figure> </figure>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ playlist.name }}</h1> <h1 class="title is-6">
{{ playlist.name }}
</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -19,5 +21,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,16 +1,44 @@
<template> <template>
<div class="media" v-if="is_next || !show_only_next_items"> <div v-if="is_next || !show_only_next_items" class="media">
<div class="media-left" v-if="edit_mode"> <div v-if="edit_mode" class="media-left">
<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"
/></span>
</div> </div>
<div class="media-content fd-has-action is-clipped" v-on:click="play"> <div class="media-content fd-has-action is-clipped" @click="play">
<h1 class="title is-6" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next }">{{ item.title }}</h1> <h1
<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 }"><b>{{ item.artist }}</b></h2> class="title is-6"
<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> :class="{
'has-text-primary': item.id === state.item_id,
'has-text-grey-light': !is_next
}"
>
{{ item.title }}
</h1>
<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
}"
>
<b>{{ item.artist }}</b>
</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">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -20,14 +48,20 @@ import webapi from '@/webapi'
export default { export default {
name: 'ListItemQueueItem', name: 'ListItemQueueItem',
props: ['item', 'position', 'current_position', 'show_only_next_items', 'edit_mode'], props: [
'item',
'position',
'current_position',
'show_only_next_items',
'edit_mode'
],
computed: { computed: {
state () { state() {
return this.$store.state.player return this.$store.state.player
}, },
is_next () { is_next() {
return this.current_position < 0 || this.position >= this.current_position return this.current_position < 0 || this.position >= this.current_position
} }
}, },
@ -40,5 +74,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,16 +1,32 @@
<template> <template>
<div class="media" :id="'index_' + track.title_sort.charAt(0).toUpperCase()" :class="{ 'with-progress': $slots.progress }"> <div
<figure class="media-left fd-has-action" v-if="$slots.icon"> :id="'index_' + track.title_sort.charAt(0).toUpperCase()"
<slot name="icon"></slot> class="media"
:class="{ 'with-progress': $slots.progress }"
>
<figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon" />
</figure> </figure>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6" :class="{ 'has-text-grey': track.media_kind === 'podcast' && track.play_count > 0 }">{{ track.title }}</h1> <h1
<h2 class="subtitle is-7 has-text-grey"><b>{{ track.artist }}</b></h2> class="title is-6"
<h2 class="subtitle is-7 has-text-grey">{{ track.album }}</h2> :class="{
<slot name="progress"></slot> 'has-text-grey':
track.media_kind === 'podcast' && track.play_count > 0
}"
>
{{ track.title }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ track.artist }}</b>
</h2>
<h2 class="subtitle is-7 has-text-grey">
{{ track.album }}
</h2>
<slot name="progress" />
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -22,5 +38,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,18 +1,36 @@
<template> <template>
<div> <div>
<list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)"> <list-item-playlist
<template v-slot:icon> v-for="playlist in playlists"
:key="playlist.id"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<template #icon>
<span class="icon"> <span class="icon">
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i> <i
class="mdi"
:class="{
'mdi-library-music': playlist.type !== 'folder',
'mdi-rss': playlist.type === 'rss',
'mdi-folder': playlist.type === 'folder'
}"
/>
</span> </span>
</template> </template>
<template v-slot:actions> <template #actions>
<a @click.prevent.stop="open_dialog(playlist)"> <a @click.prevent.stop="open_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-playlist> </list-item-playlist>
<modal-dialog-playlist :show="show_details_modal" :playlist="selected_playlist" @close="show_details_modal = false" /> <modal-dialog-playlist
:show="show_details_modal"
:playlist="selected_playlist"
@close="show_details_modal = false"
/>
</div> </div>
</template> </template>
@ -26,7 +44,7 @@ export default {
props: ['playlists'], props: ['playlists'],
data () { data() {
return { return {
show_details_modal: false, show_details_modal: false,
selected_playlist: {} selected_playlist: {}
@ -50,5 +68,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,13 +1,24 @@
<template> <template>
<div> <div>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index, track)"> <list-item-track
<template v-slot:actions> v-for="(track, index) in tracks"
:key="track.id"
:track="track"
@click="play_track(index, track)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-track> </list-item-track>
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" /> <modal-dialog-track
:show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
/>
</div> </div>
</template> </template>
@ -22,7 +33,7 @@ export default {
props: ['tracks', 'uris', 'expression'], props: ['tracks', 'uris', 'expression'],
data () { data() {
return { return {
show_details_modal: false, show_details_modal: false,
selected_track: {} selected_track: {}
@ -48,5 +59,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,30 +1,47 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4" v-if="title"> <p v-if="title" class="title is-4">
{{ title }} {{ title }}
</p> </p>
<slot name="modal-content"></slot> <slot name="modal-content" />
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="$emit('close')"> <a class="card-footer-item has-text-dark" @click="$emit('close')">
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">{{ close_action ? close_action : 'Cancel' }}</span> <span class="icon"><i class="mdi mdi-cancel" /></span>
<span class="is-size-7">{{
close_action ? close_action : 'Cancel'
}}</span>
</a> </a>
<a v-if="delete_action" class="card-footer-item has-background-danger has-text-white has-text-weight-bold" @click="$emit('delete')"> <a
<span class="icon"><i class="mdi mdi-delete"></i></span> <span class="is-size-7">{{ delete_action }}</span> v-if="delete_action"
class="card-footer-item has-background-danger has-text-white has-text-weight-bold"
@click="$emit('delete')"
>
<span class="icon"><i class="mdi mdi-delete" /></span>
<span class="is-size-7">{{ delete_action }}</span>
</a> </a>
<a v-if="ok_action" class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="$emit('ok')"> <a
<span class="icon"><i class="mdi mdi-check"></i></span> <span class="is-size-7">{{ ok_action }}</span> v-if="ok_action"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="$emit('ok')"
>
<span class="icon"><i class="mdi mdi-check" /></span>
<span class="is-size-7">{{ ok_action }}</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -37,5 +54,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -10,32 +10,54 @@
<form @submit.prevent="add_stream"> <form @submit.prevent="add_stream">
<div class="field"> <div class="field">
<p class="control is-expanded has-icons-left"> <p class="control is-expanded has-icons-left">
<input class="input is-shadowless" type="text" placeholder="http://url-to-rss" v-model="url" :disabled="loading" ref="url_field"> <input
ref="url_field"
v-model="url"
class="input is-shadowless"
type="text"
placeholder="http://url-to-rss"
:disabled="loading"
/>
<span class="icon is-left"> <span class="icon is-left">
<i class="mdi mdi-rss"></i> <i class="mdi mdi-rss" />
</span> </span>
</p> </p>
<p class="help">Adding a podcast includes creating an RSS playlist, that will allow OwnTone to manage the podcast subscription. <p class="help">
Adding a podcast includes creating an RSS playlist, that
will allow OwnTone to manage the podcast subscription.
</p> </p>
</div> </div>
</form> </form>
</div> </div>
<footer class="card-footer" v-if="loading"> <footer v-if="loading" class="card-footer">
<a class="card-footer-item button is-loading"> <a class="card-footer-item button is-loading">
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Processing ...</span> <span class="icon"><i class="mdi mdi-web" /></span>
<span class="is-size-7">Processing ...</span>
</a> </a>
</footer> </footer>
<footer class="card-footer" v-else> <footer v-else class="card-footer">
<a class="card-footer-item has-text-danger" @click="$emit('close')"> <a
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span> class="card-footer-item has-text-danger"
@click="$emit('close')"
>
<span class="icon"><i class="mdi mdi-cancel" /></span>
<span class="is-size-7">Cancel</span>
</a> </a>
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="add_stream"> <a
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span> class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="add_stream"
>
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -48,28 +70,15 @@ export default {
name: 'ModalDialogAddRss', name: 'ModalDialogAddRss',
props: ['show'], props: ['show'],
data () { data() {
return { return {
url: '', url: '',
loading: false loading: false
} }
}, },
methods: {
add_stream: function () {
this.loading = true
webapi.library_add(this.url).then(() => {
this.$emit('close')
this.$emit('podcast-added')
this.url = ''
}).catch(() => {
this.loading = false
})
}
},
watch: { watch: {
'show' () { show() {
if (this.show) { if (this.show) {
this.loading = false this.loading = false
@ -79,9 +88,24 @@ export default {
}, 10) }, 10)
} }
} }
},
methods: {
add_stream: function () {
this.loading = true
webapi
.library_add(this.url)
.then(() => {
this.$emit('close')
this.$emit('podcast-added')
this.url = ''
})
.catch(() => {
this.loading = false
})
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,44 +1,63 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">Add stream URL</p>
Add stream URL <form class="fd-has-margin-bottom" @submit.prevent="play">
</p>
<form v-on:submit.prevent="play" class="fd-has-margin-bottom">
<div class="field"> <div class="field">
<p class="control is-expanded has-icons-left"> <p class="control is-expanded has-icons-left">
<input class="input is-shadowless" type="text" placeholder="http://url-to-stream" v-model="url" :disabled="loading" ref="url_field"> <input
ref="url_field"
v-model="url"
class="input is-shadowless"
type="text"
placeholder="http://url-to-stream"
:disabled="loading"
/>
<span class="icon is-left"> <span class="icon is-left">
<i class="mdi mdi-web"></i> <i class="mdi mdi-web" />
</span> </span>
</p> </p>
</div> </div>
</form> </form>
</div> </div>
<footer class="card-footer" v-if="loading"> <footer v-if="loading" class="card-footer">
<a class="card-footer-item has-text-dark"> <a class="card-footer-item has-text-dark">
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Loading ...</span> <span class="icon"><i class="mdi mdi-web" /></span>
<span class="is-size-7">Loading ...</span>
</a> </a>
</footer> </footer>
<footer class="card-footer" v-else> <footer v-else class="card-footer">
<a class="card-footer-item has-text-danger" @click="$emit('close')"> <a
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span> class="card-footer-item has-text-danger"
@click="$emit('close')"
>
<span class="icon"><i class="mdi mdi-cancel" /></span>
<span class="is-size-7">Cancel</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="add_stream"> <a class="card-footer-item has-text-dark" @click="add_stream">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="play"> <a
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span> class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="play"
>
<span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -51,37 +70,15 @@ export default {
name: 'ModalDialogAddUrlStream', name: 'ModalDialogAddUrlStream',
props: ['show'], props: ['show'],
data () { data() {
return { return {
url: '', url: '',
loading: false loading: false
} }
}, },
methods: {
add_stream: function () {
this.loading = true
webapi.queue_add(this.url).then(() => {
this.$emit('close')
this.url = ''
}).catch(() => {
this.loading = false
})
},
play: function () {
this.loading = true
webapi.player_play_uri(this.url, false).then(() => {
this.$emit('close')
this.url = ''
}).catch(() => {
this.loading = false
})
}
},
watch: { watch: {
'show' () { show() {
if (this.show) { if (this.show) {
this.loading = false this.loading = false
@ -91,9 +88,36 @@ export default {
}, 10) }, 10)
} }
} }
},
methods: {
add_stream: function () {
this.loading = true
webapi
.queue_add(this.url)
.then(() => {
this.$emit('close')
this.url = ''
})
.catch(() => {
this.loading = false
})
},
play: function () {
this.loading = true
webapi
.player_play_uri(this.url, false)
.then(() => {
this.$emit('close')
this.url = ''
})
.catch(() => {
this.loading = false
})
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -10,22 +10,33 @@
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
class="image is-square fd-has-margin-bottom fd-has-shadow" /> class="image is-square fd-has-margin-bottom fd-has-shadow"
/>
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a> <a class="has-text-link" @click="open_album">{{
album.name
}}</a>
</p> </p>
<div class="buttons" v-if="media_kind_resolved === 'podcast'"> <div v-if="media_kind_resolved === 'podcast'" class="buttons">
<a class="button is-small" @click="mark_played">Mark as played</a> <a class="button is-small" @click="mark_played"
<a class="button is-small" @click="$emit('remove-podcast')">Remove podcast</a> >Mark as played</a
>
<a class="button is-small" @click="$emit('remove-podcast')"
>Remove podcast</a
>
</div> </div>
<div class="content is-small"> <div class="content is-small">
<p v-if="album.artist"> <p v-if="album.artist">
<span class="heading">Album artist</span> <span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a> <a class="title is-6 has-text-link" @click="open_artist">{{
album.artist
}}</a>
</p> </p>
<p v-if="album.date_released"> <p v-if="album.date_released">
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ $filters.time(album.date_released, 'L') }}</span> <span class="title is-6">{{
$filters.time(album.date_released, 'L')
}}</span>
</p> </p>
<p v-else-if="album.year > 0"> <p v-else-if="album.year > 0">
<span class="heading">Year</span> <span class="heading">Year</span>
@ -37,32 +48,45 @@
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ $filters.duration(album.length_ms) }}</span> <span class="title is-6">{{
$filters.duration(album.length_ms)
}}</span>
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>
<span class="title is-6">{{ album.media_kind }} - {{ album.data_kind }}</span> <span class="title is-6"
>{{ album.media_kind }} - {{ album.data_kind }}</span
>
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ $filters.time(album.time_added, 'L LT') }}</span> <span class="title is-6">{{
$filters.time(album.time_added, 'L LT')
}}</span>
</p> </p>
</div> </div>
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -77,7 +101,7 @@ export default {
components: { CoverArtwork }, components: { CoverArtwork },
props: ['show', 'album', 'media_kind', 'new_tracks'], props: ['show', 'album', 'media_kind', 'new_tracks'],
data () { data() {
return { return {
artwork_visible: false artwork_visible: false
} }
@ -123,17 +147,21 @@ export default {
if (this.media_kind_resolved === 'podcast') { if (this.media_kind_resolved === 'podcast') {
// No artist page for podcasts // No artist page for podcasts
} else if (this.media_kind_resolved === 'audiobook') { } else if (this.media_kind_resolved === 'audiobook') {
this.$router.push({ path: '/audiobooks/artists/' + this.album.artist_id }) this.$router.push({
path: '/audiobooks/artists/' + this.album.artist_id
})
} else { } else {
this.$router.push({ path: '/music/artists/' + this.album.artist_id }) this.$router.push({ path: '/music/artists/' + this.album.artist_id })
} }
}, },
mark_played: function () { mark_played: function () {
webapi.library_album_track_update(this.album.id, { play_count: 'played' }).then(({ data }) => { webapi
this.$emit('play-count-changed') .library_album_track_update(this.album.id, { play_count: 'played' })
this.$emit('close') .then(({ data }) => {
}) this.$emit('play-count-changed')
this.$emit('close')
})
}, },
artwork_loaded: function () { artwork_loaded: function () {
@ -147,5 +175,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,13 +1,15 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a> <a class="has-text-link" @click="open_artist">{{
artist.name
}}</a>
</p> </p>
<div class="content is-small"> <div class="content is-small">
<p> <p>
@ -24,24 +26,33 @@
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ $filters.time(artist.time_added, 'L LT') }}</span> <span class="title is-6">{{
$filters.time(artist.time_added, 'L LT')
}}</span>
</p> </p>
</div> </div>
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -78,5 +89,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,37 +1,50 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_albums">{{ composer.name }}</a> <a class="has-text-link" @click="open_albums">{{
composer.name
}}</a>
</p> </p>
<p> <p>
<span class="heading">Albums</span> <span class="heading">Albums</span>
<a class="has-text-link is-6" @click="open_albums">{{ composer.album_count }}</a> <a class="has-text-link is-6" @click="open_albums">{{
composer.album_count
}}</a>
</p> </p>
<p> <p>
<span class="heading">Tracks</span> <span class="heading">Tracks</span>
<a class="has-text-link is-6" @click="open_tracks">{{ composer.track_count }}</a> <a class="has-text-link is-6" @click="open_tracks">{{
composer.track_count
}}</a>
</p> </p>
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -47,31 +60,43 @@ export default {
methods: { methods: {
play: function () { play: function () {
this.$emit('close') this.$emit('close')
webapi.player_play_expression('composer is "' + this.composer.name + '" and media_kind is music', false) webapi.player_play_expression(
'composer is "' + this.composer.name + '" and media_kind is music',
false
)
}, },
queue_add: function () { queue_add: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add('composer is "' + this.composer.name + '" and media_kind is music') webapi.queue_expression_add(
'composer is "' + this.composer.name + '" and media_kind is music'
)
}, },
queue_add_next: function () { queue_add_next: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add_next('composer is "' + this.composer.name + '" and media_kind is music') webapi.queue_expression_add_next(
'composer is "' + this.composer.name + '" and media_kind is music'
)
}, },
open_albums: function () { open_albums: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ name: 'ComposerAlbums', params: { composer: this.composer.name } }) this.$router.push({
name: 'ComposerAlbums',
params: { composer: this.composer.name }
})
}, },
open_tracks: function () { open_tracks: function () {
this.show_details_modal = false this.show_details_modal = false
this.$router.push({ name: 'ComposerTracks', params: { composer: this.composer.name } }) this.$router.push({
name: 'ComposerTracks',
params: { composer: this.composer.name }
})
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -12,18 +12,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -39,21 +46,27 @@ export default {
methods: { methods: {
play: function () { play: function () {
this.$emit('close') this.$emit('close')
webapi.player_play_expression('path starts with "' + this.directory.path + '" order by path asc', false) webapi.player_play_expression(
'path starts with "' + this.directory.path + '" order by path asc',
false
)
}, },
queue_add: function () { queue_add: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add('path starts with "' + this.directory.path + '" order by path asc') webapi.queue_expression_add(
'path starts with "' + this.directory.path + '" order by path asc'
)
}, },
queue_add_next: function () { queue_add_next: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add_next('path starts with "' + this.directory.path + '" order by path asc') webapi.queue_expression_add_next(
'path starts with "' + this.directory.path + '" order by path asc'
)
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,29 +1,38 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_genre">{{ genre.name }}</a> <a class="has-text-link" @click="open_genre">{{
genre.name
}}</a>
</p> </p>
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -39,17 +48,24 @@ export default {
methods: { methods: {
play: function () { play: function () {
this.$emit('close') this.$emit('close')
webapi.player_play_expression('genre is "' + this.genre.name + '" and media_kind is music', false) webapi.player_play_expression(
'genre is "' + this.genre.name + '" and media_kind is music',
false
)
}, },
queue_add: function () { queue_add: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add('genre is "' + this.genre.name + '" and media_kind is music') webapi.queue_expression_add(
'genre is "' + this.genre.name + '" and media_kind is music'
)
}, },
queue_add_next: function () { queue_add_next: function () {
this.$emit('close') this.$emit('close')
webapi.queue_expression_add_next('genre is "' + this.genre.name + '" and media_kind is music') webapi.queue_expression_add_next(
'genre is "' + this.genre.name + '" and media_kind is music'
)
}, },
open_genre: function () { open_genre: function () {
@ -60,5 +76,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,13 +1,15 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a> <a class="has-text-link" @click="open_playlist">{{
playlist.name
}}</a>
</p> </p>
<div class="content is-small"> <div class="content is-small">
<p> <p>
@ -20,20 +22,27 @@
</p> </p>
</div> </div>
</div> </div>
<footer class="card-footer" v-if="!playlist.folder"> <footer v-if="!playlist.folder" class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -70,5 +79,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,41 +1,59 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">Save queue to playlist</p>
Save queue to playlist <form class="fd-has-margin-bottom" @submit.prevent="save">
</p>
<form v-on:submit.prevent="save" class="fd-has-margin-bottom">
<div class="field"> <div class="field">
<p class="control is-expanded has-icons-left"> <p class="control is-expanded has-icons-left">
<input class="input is-shadowless" type="text" placeholder="Playlist name" v-model="playlist_name" :disabled="loading" ref="playlist_name_field"> <input
ref="playlist_name_field"
v-model="playlist_name"
class="input is-shadowless"
type="text"
placeholder="Playlist name"
:disabled="loading"
/>
<span class="icon is-left"> <span class="icon is-left">
<i class="mdi mdi-file-music"></i> <i class="mdi mdi-file-music" />
</span> </span>
</p> </p>
</div> </div>
</form> </form>
</div> </div>
<footer class="card-footer" v-if="loading"> <footer v-if="loading" class="card-footer">
<a class="card-footer-item has-text-dark"> <a class="card-footer-item has-text-dark">
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Saving ...</span> <span class="icon"><i class="mdi mdi-web" /></span>
<span class="is-size-7">Saving ...</span>
</a> </a>
</footer> </footer>
<footer class="card-footer" v-else> <footer v-else class="card-footer">
<a class="card-footer-item has-text-danger" @click="$emit('close')"> <a
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span> class="card-footer-item has-text-danger"
@click="$emit('close')"
>
<span class="icon"><i class="mdi mdi-cancel" /></span>
<span class="is-size-7">Cancel</span>
</a> </a>
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="save"> <a
<span class="icon"><i class="mdi mdi-content-save"></i></span> <span class="is-size-7">Save</span> class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="save"
>
<span class="icon"><i class="mdi mdi-content-save" /></span>
<span class="is-size-7">Save</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -48,13 +66,26 @@ export default {
name: 'ModalDialogPlaylistSave', name: 'ModalDialogPlaylistSave',
props: ['show'], props: ['show'],
data () { data() {
return { return {
playlist_name: '', playlist_name: '',
loading: false loading: false
} }
}, },
watch: {
show() {
if (this.show) {
this.loading = false
// We need to delay setting the focus to the input field until the field is part of the dom and visible
setTimeout(() => {
this.$refs.playlist_name_field.focus()
}, 10)
}
}
},
methods: { methods: {
save: function () { save: function () {
if (this.playlist_name.length < 1) { if (this.playlist_name.length < 1) {
@ -62,29 +93,18 @@ export default {
} }
this.loading = true this.loading = true
webapi.queue_save_playlist(this.playlist_name).then(() => { webapi
this.$emit('close') .queue_save_playlist(this.playlist_name)
this.playlist_name = '' .then(() => {
}).catch(() => { this.$emit('close')
this.loading = false this.playlist_name = ''
}) })
} .catch(() => {
}, this.loading = false
})
watch: {
'show' () {
if (this.show) {
this.loading = false
// We need to delay setting the focus to the input field until the field is part of the dom and visible
setTimeout(() => {
this.$refs.playlist_name_field.focus()
}, 10)
}
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -15,12 +15,22 @@
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Album</span> <span class="heading">Album</span>
<a v-if="item.album_id" class="title is-6 has-text-link" @click="open_album">{{ item.album }}</a> <a
v-if="item.album_id"
class="title is-6 has-text-link"
@click="open_album"
>{{ item.album }}</a
>
<span v-else class="title is-6">{{ item.album }}</span> <span v-else class="title is-6">{{ item.album }}</span>
</p> </p>
<p v-if="item.album_artist"> <p v-if="item.album_artist">
<span class="heading">Album artist</span> <span class="heading">Album artist</span>
<a v-if="item.album_artist_id" class="title is-6 has-text-link" @click="open_album_artist">{{ item.album_artist }}</a> <a
v-if="item.album_artist_id"
class="title is-6 has-text-link"
@click="open_album_artist"
>{{ item.album_artist }}</a
>
<span v-else class="title is-6">{{ item.album_artist }}</span> <span v-else class="title is-6">{{ item.album_artist }}</span>
</p> </p>
<p v-if="item.composer"> <p v-if="item.composer">
@ -33,15 +43,21 @@
</p> </p>
<p v-if="item.genre"> <p v-if="item.genre">
<span class="heading">Genre</span> <span class="heading">Genre</span>
<a class="title is-6 has-text-link" @click="open_genre">{{ item.genre }}</a> <a class="title is-6 has-text-link" @click="open_genre">{{
item.genre
}}</a>
</p> </p>
<p> <p>
<span class="heading">Track / Disc</span> <span class="heading">Track / Disc</span>
<span class="title is-6">{{ item.track_number }} / {{ item.disc_number }}</span> <span class="title is-6"
>{{ item.track_number }} / {{ item.disc_number }}</span
>
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ $filters.duration(item.length_ms) }}</span> <span class="title is-6">{{
$filters.duration(item.length_ms)
}}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>
@ -49,14 +65,26 @@
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>
<span class="title is-6">{{ item.media_kind }} - {{ item.data_kind }} <span class="has-text-weight-normal" v-if="item.data_kind === 'spotify'">(<a @click="open_spotify_artist">artist</a>, <a @click="open_spotify_album">album</a>)</span></span> <span class="title is-6"
>{{ item.media_kind }} - {{ item.data_kind }}
<span
v-if="item.data_kind === 'spotify'"
class="has-text-weight-normal"
>(<a @click="open_spotify_artist">artist</a>,
<a @click="open_spotify_album">album</a>)</span
></span
>
</p> </p>
<p> <p>
<span class="heading">Quality</span> <span class="heading">Quality</span>
<span class="title is-6"> <span class="title is-6">
{{ item.type }} {{ item.type }}
<span v-if="item.samplerate"> | {{ item.samplerate }} Hz</span> <span v-if="item.samplerate">
<span v-if="item.channels"> | {{ $filters.channels(item.channels) }}</span> | {{ item.samplerate }} Hz</span
>
<span v-if="item.channels">
| {{ $filters.channels(item.channels) }}</span
>
<span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span> <span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span>
</span> </span>
</p> </p>
@ -64,15 +92,21 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="remove"> <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> <span class="icon"><i class="mdi mdi-delete" /></span>
<span class="is-size-7">Remove</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -86,12 +120,28 @@ export default {
name: 'ModalDialogQueueItem', name: 'ModalDialogQueueItem',
props: ['show', 'item'], props: ['show', 'item'],
data () { data() {
return { return {
spotify_track: {} spotify_track: {}
} }
}, },
watch: {
item() {
if (this.item && this.item.data_kind === 'spotify') {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi
.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1))
.then((response) => {
this.spotify_track = response
})
} else {
this.spotify_track = {}
}
}
},
methods: { methods: {
remove: function () { remove: function () {
this.$emit('close') this.$emit('close')
@ -123,30 +173,19 @@ export default {
open_spotify_artist: function () { open_spotify_artist: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ path: '/music/spotify/artists/' + this.spotify_track.artists[0].id }) this.$router.push({
path: '/music/spotify/artists/' + this.spotify_track.artists[0].id
})
}, },
open_spotify_album: function () { open_spotify_album: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ path: '/music/spotify/albums/' + this.spotify_track.album.id }) this.$router.push({
} path: '/music/spotify/albums/' + this.spotify_track.album.id
}, })
watch: {
'item' () {
if (this.item && this.item.data_kind === 'spotify') {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1)).then((response) => {
this.spotify_track = response
})
} else {
this.spotify_track = {}
}
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,36 +1,52 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">Remote pairing request</p>
Remote pairing request <form @submit.prevent="kickoff_pairing">
</p>
<form v-on:submit.prevent="kickoff_pairing">
<label class="label"> <label class="label">
{{ pairing.remote }} {{ pairing.remote }}
</label> </label>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" type="text" placeholder="Enter pairing code" v-model="pairing_req.pin" ref="pin_field"> <input
ref="pin_field"
v-model="pairing_req.pin"
class="input"
type="text"
placeholder="Enter pairing code"
/>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-danger" @click="$emit('close')"> <a
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span> class="card-footer-item has-text-danger"
@click="$emit('close')"
>
<span class="icon"><i class="mdi mdi-cancel" /></span>
<span class="is-size-7">Cancel</span>
</a> </a>
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="kickoff_pairing"> <a
<span class="icon"><i class="mdi mdi-cellphone-iphone"></i></span> <span class="is-size-7">Pair Remote</span> class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="kickoff_pairing"
>
<span class="icon"><i class="mdi mdi-cellphone-iphone" /></span>
<span class="is-size-7">Pair Remote</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -43,28 +59,20 @@ export default {
name: 'ModalDialogRemotePairing', name: 'ModalDialogRemotePairing',
props: ['show'], props: ['show'],
data () { data() {
return { return {
pairing_req: { pin: '' } pairing_req: { pin: '' }
} }
}, },
computed: { computed: {
pairing () { pairing() {
return this.$store.state.pairing return this.$store.state.pairing
} }
}, },
methods: {
kickoff_pairing () {
webapi.pairing_kickoff(this.pairing_req).then(() => {
this.pairing_req.pin = ''
})
}
},
watch: { watch: {
'show' () { show() {
if (this.show) { if (this.show) {
this.loading = false this.loading = false
@ -74,9 +82,16 @@ export default {
}, 10) }, 10)
} }
} }
},
methods: {
kickoff_pairing() {
webapi.pairing_kickoff(this.pairing_req).then(() => {
this.pairing_req.pin = ''
})
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -12,18 +12,34 @@
<p class="subtitle"> <p class="subtitle">
{{ track.artist }} {{ track.artist }}
</p> </p>
<div class="buttons" v-if="track.media_kind === 'podcast'"> <div v-if="track.media_kind === 'podcast'" class="buttons">
<a class="button is-small" v-if="track.play_count > 0" @click="mark_new">Mark as new</a> <a
<a class="button is-small" v-if="track.play_count === 0" @click="mark_played">Mark as played</a> v-if="track.play_count > 0"
class="button is-small"
@click="mark_new"
>Mark as new</a
>
<a
v-if="track.play_count === 0"
class="button is-small"
@click="mark_played"
>Mark as played</a
>
</div> </div>
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Album</span> <span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ track.album }}</a> <a class="title is-6 has-text-link" @click="open_album">{{
track.album
}}</a>
</p> </p>
<p v-if="track.album_artist && track.media_kind !== 'audiobook'"> <p
v-if="track.album_artist && track.media_kind !== 'audiobook'"
>
<span class="heading">Album artist</span> <span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ track.album_artist }}</a> <a class="title is-6 has-text-link" @click="open_artist">{{
track.album_artist
}}</a>
</p> </p>
<p v-if="track.composer"> <p v-if="track.composer">
<span class="heading">Composer</span> <span class="heading">Composer</span>
@ -31,7 +47,9 @@
</p> </p>
<p v-if="track.date_released"> <p v-if="track.date_released">
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ $filters.time(track.date_released, 'L') }}</span> <span class="title is-6">{{
$filters.time(track.date_released, 'L')
}}</span>
</p> </p>
<p v-else-if="track.year > 0"> <p v-else-if="track.year > 0">
<span class="heading">Year</span> <span class="heading">Year</span>
@ -39,15 +57,21 @@
</p> </p>
<p v-if="track.genre"> <p v-if="track.genre">
<span class="heading">Genre</span> <span class="heading">Genre</span>
<a class="title is-6 has-text-link" @click="open_genre">{{ track.genre }}</a> <a class="title is-6 has-text-link" @click="open_genre">{{
track.genre
}}</a>
</p> </p>
<p> <p>
<span class="heading">Track / Disc</span> <span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span> <span class="title is-6"
>{{ track.track_number }} / {{ track.disc_number }}</span
>
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ $filters.duration(track.length_ms) }}</span> <span class="title is-6">{{
$filters.duration(track.length_ms)
}}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>
@ -55,24 +79,42 @@
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }} <span class="has-text-weight-normal" v-if="track.data_kind === 'spotify'">(<a @click="open_spotify_artist">artist</a>, <a @click="open_spotify_album">album</a>)</span></span> <span class="title is-6"
>{{ track.media_kind }} - {{ track.data_kind }}
<span
v-if="track.data_kind === 'spotify'"
class="has-text-weight-normal"
>(<a @click="open_spotify_artist">artist</a>,
<a @click="open_spotify_album">album</a>)</span
></span
>
</p> </p>
<p> <p>
<span class="heading">Quality</span> <span class="heading">Quality</span>
<span class="title is-6"> <span class="title is-6">
{{ track.type }} {{ track.type }}
<span v-if="track.samplerate"> | {{ track.samplerate }} Hz</span> <span v-if="track.samplerate">
<span v-if="track.channels"> | {{ $filters.channels(track.channels) }}</span> | {{ track.samplerate }} Hz</span
<span v-if="track.bitrate"> | {{ track.bitrate }} Kb/s</span> >
<span v-if="track.channels">
| {{ $filters.channels(track.channels) }}</span
>
<span v-if="track.bitrate">
| {{ track.bitrate }} Kb/s</span
>
</span> </span>
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ $filters.time(track.time_added, 'L LT') }}</span> <span class="title is-6">{{
$filters.time(track.time_added, 'L LT')
}}</span>
</p> </p>
<p> <p>
<span class="heading">Rating</span> <span class="heading">Rating</span>
<span class="title is-6">{{ Math.floor(track.rating / 10) }} / 10</span> <span class="title is-6"
>{{ Math.floor(track.rating / 10) }} / 10</span
>
</p> </p>
<p v-if="track.comment"> <p v-if="track.comment">
<span class="heading">Comment</span> <span class="heading">Comment</span>
@ -82,18 +124,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play_track"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -108,12 +157,28 @@ export default {
props: ['show', 'track'], props: ['show', 'track'],
data () { data() {
return { return {
spotify_track: {} spotify_track: {}
} }
}, },
watch: {
track() {
if (this.track && this.track.data_kind === 'spotify') {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi
.getTrack(this.track.path.slice(this.track.path.lastIndexOf(':') + 1))
.then((response) => {
this.spotify_track = response
})
} else {
this.spotify_track = {}
}
}
},
methods: { methods: {
play_track: function () { play_track: function () {
this.$emit('close') this.$emit('close')
@ -143,7 +208,9 @@ export default {
open_artist: function () { open_artist: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ path: '/music/artists/' + this.track.album_artist_id }) this.$router.push({
path: '/music/artists/' + this.track.album_artist_id
})
}, },
open_genre: function () { open_genre: function () {
@ -152,44 +219,37 @@ export default {
open_spotify_artist: function () { open_spotify_artist: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ path: '/music/spotify/artists/' + this.spotify_track.artists[0].id }) this.$router.push({
path: '/music/spotify/artists/' + this.spotify_track.artists[0].id
})
}, },
open_spotify_album: function () { open_spotify_album: function () {
this.$emit('close') this.$emit('close')
this.$router.push({ path: '/music/spotify/albums/' + this.spotify_track.album.id }) this.$router.push({
path: '/music/spotify/albums/' + this.spotify_track.album.id
})
}, },
mark_new: function () { mark_new: function () {
webapi.library_track_update(this.track.id, { play_count: 'reset' }).then(() => { webapi
this.$emit('play-count-changed') .library_track_update(this.track.id, { play_count: 'reset' })
this.$emit('close') .then(() => {
}) this.$emit('play-count-changed')
this.$emit('close')
})
}, },
mark_played: function () { mark_played: function () {
webapi.library_track_update(this.track.id, { play_count: 'increment' }).then(() => { webapi
this.$emit('play-count-changed') .library_track_update(this.track.id, { play_count: 'increment' })
this.$emit('close') .then(() => {
}) this.$emit('play-count-changed')
} this.$emit('close')
},
watch: {
'track' () {
if (this.track && this.track.data_kind === 'spotify') {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getTrack(this.track.path.slice(this.track.path.lastIndexOf(':') + 1)).then((response) => {
this.spotify_track = response
}) })
} else {
this.spotify_track = {}
}
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,29 +1,34 @@
<template> <template>
<modal-dialog <modal-dialog
:show="show" :show="show"
title="Update library" title="Update library"
:ok_action="library.updating ? '' : 'Rescan'" :ok_action="library.updating ? '' : 'Rescan'"
close_action="Close" close_action="Close"
@ok="update_library" @ok="update_library"
@close="close()"> @close="close()"
<template v-slot:modal-content> >
<template #modal-content>
<div v-if="!library.updating"> <div v-if="!library.updating">
<p class="mb-3">Scan for new, deleted and modified files</p> <p class="mb-3">Scan for new, deleted and modified files</p>
<div class="field" v-if="spotify_enabled || rss.tracks > 0"> <div v-if="spotify_enabled || rss.tracks > 0" class="field">
<div class="control"> <div class="control">
<div class="select is-small"> <div class="select is-small">
<select v-model="update_dialog_scan_kind"> <select v-model="update_dialog_scan_kind">
<option value="">Update everything</option> <option value="">Update everything</option>
<option value="files">Only update local library</option> <option value="files">Only update local library</option>
<option value="spotify" v-if="spotify_enabled">Only update Spotify</option> <option v-if="spotify_enabled" value="spotify">
<option value="rss" v-if="rss.tracks > 0">Only update RSS feeds</option> Only update Spotify
</option>
<option v-if="rss.tracks > 0" value="rss">
Only update RSS feeds
</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="checkbox is-size-7 is-small"> <label class="checkbox is-size-7 is-small">
<input type="checkbox" v-model="rescan_metadata"> <input v-model="rescan_metadata" type="checkbox" />
Rescan metadata for unmodified files Rescan metadata for unmodified files
</label> </label>
</div> </div>
@ -45,37 +50,37 @@ export default {
components: { ModalDialog }, components: { ModalDialog },
props: ['show'], props: ['show'],
data () { data() {
return { return {
rescan_metadata: false rescan_metadata: false
} }
}, },
computed: { computed: {
library () { library() {
return this.$store.state.library return this.$store.state.library
}, },
rss () { rss() {
return this.$store.state.rss_count return this.$store.state.rss_count
}, },
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
}, },
update_dialog_scan_kind: { update_dialog_scan_kind: {
get () { get() {
return this.$store.state.update_dialog_scan_kind return this.$store.state.update_dialog_scan_kind
}, },
set (value) { set(value) {
this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, value) this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, value)
} }
} }
}, },
methods: { methods: {
update_library () { update_library() {
if (this.rescan_metadata) { if (this.rescan_metadata) {
webapi.library_rescan(this.update_dialog_scan_kind) webapi.library_rescan(this.update_dialog_scan_kind)
} else { } else {
@ -83,7 +88,7 @@ export default {
} }
}, },
close () { close() {
this.update_dialog_scan_kind = '' this.update_dialog_scan_kind = ''
this.$emit('close') this.$emit('close')
} }
@ -91,5 +96,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,65 +1,140 @@
<template> <template>
<nav class="fd-bottom-navbar navbar is-white is-fixed-bottom" :style="zindex" :class="{ 'is-transparent': is_now_playing_page, 'is-dark': !is_now_playing_page }" role="navigation" aria-label="player controls"> <nav
class="fd-bottom-navbar navbar is-white is-fixed-bottom"
:style="zindex"
:class="{
'is-transparent': is_now_playing_page,
'is-dark': !is_now_playing_page
}"
role="navigation"
aria-label="player controls"
>
<div class="navbar-brand fd-expanded"> <div class="navbar-brand fd-expanded">
<!-- Link to queue --> <!-- Link to queue -->
<navbar-item-link to="/" exact> <navbar-item-link to="/" exact>
<span class="icon"><i class="mdi mdi-24px mdi-playlist-play"></i></span> <span class="icon"><i class="mdi mdi-24px mdi-playlist-play" /></span>
</navbar-item-link> </navbar-item-link>
<!-- Now playing artist/title (not visible on "now playing" page) --> <!-- Now playing artist/title (not visible on "now playing" page) -->
<router-link to="/now-playing" v-if="!is_now_playing_page" class="navbar-item is-expanded is-clipped" active-class="is-active" exact> <router-link
v-if="!is_now_playing_page"
to="/now-playing"
class="navbar-item is-expanded is-clipped"
active-class="is-active"
exact
>
<div class="is-clipped"> <div class="is-clipped">
<p class="is-size-7 fd-is-text-clipped"> <p class="is-size-7 fd-is-text-clipped">
<strong>{{ now_playing.title }}</strong><br> <strong>{{ now_playing.title }}</strong
{{ now_playing.artist }}<span v-if="now_playing.data_kind === 'url'"> - {{ now_playing.album }}</span> ><br />
{{ now_playing.artist
}}<span v-if="now_playing.data_kind === 'url'">
- {{ now_playing.album }}</span
>
</p> </p>
</div> </div>
</router-link> </router-link>
<!-- Skip previous (not visible on "now playing" page) --> <!-- Skip previous (not visible on "now playing" page) -->
<player-button-previous v-if="is_now_playing_page" class="navbar-item fd-margin-left-auto" icon_style="mdi-24px"></player-button-previous> <player-button-previous
<player-button-seek-back v-if="is_now_playing_page" seek_ms="10000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-back> v-if="is_now_playing_page"
class="navbar-item fd-margin-left-auto"
icon_style="mdi-24px"
/>
<player-button-seek-back
v-if="is_now_playing_page"
seek_ms="10000"
class="navbar-item"
icon_style="mdi-24px"
/>
<!-- Play/pause --> <!-- Play/pause -->
<player-button-play-pause class="navbar-item" icon_style="mdi-36px" show_disabled_message></player-button-play-pause> <player-button-play-pause
<player-button-seek-forward v-if="is_now_playing_page" seek_ms="30000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-forward> class="navbar-item"
icon_style="mdi-36px"
show_disabled_message
/>
<player-button-seek-forward
v-if="is_now_playing_page"
seek_ms="30000"
class="navbar-item"
icon_style="mdi-24px"
/>
<!-- Skip next (not visible on "now playing" page) --> <!-- Skip next (not visible on "now playing" page) -->
<player-button-next v-if="is_now_playing_page" class="navbar-item" icon_style="mdi-24px"></player-button-next> <player-button-next
v-if="is_now_playing_page"
class="navbar-item"
icon_style="mdi-24px"
/>
<!-- Player menu button (only visible on mobile and tablet) --> <!-- Player menu button (only visible on mobile and tablet) -->
<a class="navbar-item fd-margin-left-auto is-hidden-desktop" @click="show_player_menu = !show_player_menu"> <a
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-chevron-up': !show_player_menu, 'mdi-chevron-down': show_player_menu }"></i></span> class="navbar-item fd-margin-left-auto is-hidden-desktop"
@click="show_player_menu = !show_player_menu"
>
<span class="icon"
><i
class="mdi mdi-18px"
:class="{
'mdi-chevron-up': !show_player_menu,
'mdi-chevron-down': show_player_menu
}"
/></span>
</a> </a>
<!-- Player menu dropup menu (only visible on desktop) --> <!-- Player menu dropup menu (only visible on desktop) -->
<div class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch" <div
:class="{ 'is-active': show_player_menu }"> class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch"
<a class="navbar-link is-arrowless" :class="{ 'is-active': show_player_menu }"
@click="show_player_menu = !show_player_menu"> >
<span class="icon"><i class="mdi mdi-18px" <a
:class="{ 'mdi-chevron-up': !show_player_menu, 'mdi-chevron-down': show_player_menu }"></i></span> class="navbar-link is-arrowless"
@click="show_player_menu = !show_player_menu"
>
<span class="icon"
><i
class="mdi mdi-18px"
:class="{
'mdi-chevron-up': !show_player_menu,
'mdi-chevron-down': show_player_menu
}"
/></span>
</a> </a>
<div class="navbar-dropdown is-right is-boxed" style="margin-right: 6px; margin-bottom: 6px; border-radius: 6px;"> <div
class="navbar-dropdown is-right is-boxed"
style="margin-right: 6px; margin-bottom: 6px; border-radius: 6px"
>
<div class="navbar-item"> <div class="navbar-item">
<!-- Outputs: master volume --> <!-- Outputs: master volume -->
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0">
<a class="button is-white is-small" @click="toggle_mute_volume"> <a
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-volume-off': player.volume <= 0, 'mdi-volume-high': player.volume > 0 }"></i></span> class="button is-white is-small"
@click="toggle_mute_volume"
>
<span class="icon"
><i
class="mdi mdi-18px"
:class="{
'mdi-volume-off': player.volume <= 0,
'mdi-volume-high': player.volume > 0
}"
/></span>
</a> </a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading">Volume</p> <p class="heading">Volume</p>
<Slider v-model="player.volume" <Slider
v-model="player.volume"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
:tooltips="false" :tooltips="false"
:classes="{ target: 'slider' }"
@change="set_volume" @change="set_volume"
:classes="{ target: 'slider'}" /> />
<!--range-slider <!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
@ -75,28 +150,53 @@
</div> </div>
<!-- Outputs: master volume --> <!-- Outputs: master volume -->
<hr class="fd-navbar-divider"> <hr class="fd-navbar-divider" />
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output> <navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume --> <!-- Outputs: stream volume -->
<hr class="fd-navbar-divider"> <hr class="fd-navbar-divider" />
<div class="navbar-item"> <div class="navbar-item">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0">
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a> <a
class="button is-white is-small"
:class="{ 'is-loading': loading }"
><span
class="icon fd-has-action"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
><i class="mdi mdi-18px mdi-radio-tower" /></span
></a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p> <p
<Slider v-model="stream_volume" class="heading"
:class="{ 'has-text-grey-light': !playing }"
>
HTTP stream
<a href="stream.mp3"
><span class="is-lowercase">(stream.mp3)</span></a
>
</p>
<Slider
v-model="stream_volume"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
:tooltips="false" :tooltips="false"
:disabled="!playing" :disabled="!playing"
:classes="{ target: 'slider' }"
@change="set_stream_volume" @change="set_stream_volume"
:classes="{ target: 'slider'}" /> />
<!--range-slider <!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
@ -113,14 +213,14 @@
</div> </div>
<!-- Playback controls --> <!-- Playback controls -->
<hr class="fd-navbar-divider"> <hr class="fd-navbar-divider" />
<div class="navbar-item"> <div class="navbar-item">
<div class="level is-mobile fd-expanded"> <div class="level is-mobile fd-expanded">
<div class="level-item"> <div class="level-item">
<div class="buttons has-addons"> <div class="buttons has-addons">
<player-button-repeat class="button"></player-button-repeat> <player-button-repeat class="button" />
<player-button-shuffle class="button"></player-button-shuffle> <player-button-shuffle class="button" />
<player-button-consume class="button"></player-button-consume> <player-button-consume class="button" />
</div> </div>
</div> </div>
</div> </div>
@ -130,40 +230,51 @@
</div> </div>
<!-- Player menu (only visible on mobile and tablet) --> <!-- Player menu (only visible on mobile and tablet) -->
<div class="navbar-menu is-hidden-desktop" :class="{ 'is-active': show_player_menu }"> <div
<div class="navbar-start"> class="navbar-menu is-hidden-desktop"
</div> :class="{ 'is-active': show_player_menu }"
>
<div class="navbar-start" />
<div class="navbar-end"> <div class="navbar-end">
<!-- Repeat/shuffle/consume --> <!-- Repeat/shuffle/consume -->
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons is-centered"> <div class="buttons is-centered">
<player-button-repeat class="button" icon_style="mdi-18px"></player-button-repeat> <player-button-repeat class="button" icon_style="mdi-18px" />
<player-button-shuffle class="button" icon_style="mdi-18px"></player-button-shuffle> <player-button-shuffle class="button" icon_style="mdi-18px" />
<player-button-consume class="button" icon_style="mdi-18px"></player-button-consume> <player-button-consume class="button" icon_style="mdi-18px" />
</div> </div>
</div> </div>
<hr class="fd-navbar-divider"> <hr class="fd-navbar-divider" />
<!-- Outputs: master volume --> <!-- Outputs: master volume -->
<div class="navbar-item"> <div class="navbar-item">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0">
<a class="button is-white is-small" @click="toggle_mute_volume"> <a class="button is-white is-small" @click="toggle_mute_volume">
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-volume-off': player.volume <= 0, 'mdi-volume-high': player.volume > 0 }"></i></span> <span class="icon"
><i
class="mdi mdi-18px"
:class="{
'mdi-volume-off': player.volume <= 0,
'mdi-volume-high': player.volume > 0
}"
/></span>
</a> </a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading">Volume</p> <p class="heading">Volume</p>
<Slider v-model="player.volume" <Slider
v-model="player.volume"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
:tooltips="false" :tooltips="false"
:classes="{ target: 'slider' }"
@change="set_volume" @change="set_volume"
:classes="{ target: 'slider'}" /> />
<!--range-slider <!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
@ -179,32 +290,54 @@
</div> </div>
<!-- Outputs: speaker volumes --> <!-- Outputs: speaker volumes -->
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output> <navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume --> <!-- Outputs: stream volume -->
<hr class="fd-navbar-divider"> <hr class="fd-navbar-divider" />
<div class="navbar-item fd-has-margin-bottom"> <div class="navbar-item fd-has-margin-bottom">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0">
<a class="button is-white is-small" :class="{ 'is-loading': loading }"> <a
<span class="icon fd-has-action" class="button is-white is-small"
:class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" :class="{ 'is-loading': loading }"
@click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i> >
<span
class="icon fd-has-action"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
><i class="mdi mdi-18px mdi-radio-tower" />
</span> </span>
</a> </a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p> <p
<Slider v-model="stream_volume" class="heading"
:class="{ 'has-text-grey-light': !playing }"
>
HTTP stream
<a href="stream.mp3"
><span class="is-lowercase">(stream.mp3)</span></a
>
</p>
<Slider
v-model="stream_volume"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
:tooltips="false" :tooltips="false"
:disabled="!playing" :disabled="!playing"
:classes="{ target: 'slider' }"
@change="set_stream_volume" @change="set_stream_volume"
:classes="{ target: 'slider'}" /> />
<!-- range-slider <!-- range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
@ -258,7 +391,7 @@ export default {
PlayerButtonSeekBack PlayerButtonSeekBack
}, },
data () { data() {
return { return {
old_volume: 0, old_volume: 0,
@ -273,49 +406,67 @@ export default {
computed: { computed: {
show_player_menu: { show_player_menu: {
get () { get() {
return this.$store.state.show_player_menu return this.$store.state.show_player_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_PLAYER_MENU, value) this.$store.commit(types.SHOW_PLAYER_MENU, value)
} }
}, },
show_burger_menu () { show_burger_menu() {
return this.$store.state.show_burger_menu return this.$store.state.show_burger_menu
}, },
zindex () { zindex() {
if (this.show_burger_menu) { if (this.show_burger_menu) {
return 'z-index: 20' return 'z-index: 20'
} }
return '' return ''
}, },
state () { state() {
return this.$store.state.player return this.$store.state.player
}, },
now_playing () { now_playing() {
return this.$store.getters.now_playing return this.$store.getters.now_playing
}, },
is_now_playing_page () { is_now_playing_page() {
return this.$route.path === '/now-playing' return this.$route.path === '/now-playing'
}, },
outputs () { outputs() {
return this.$store.state.outputs return this.$store.state.outputs
}, },
player () { player() {
return this.$store.state.player return this.$store.state.player
}, },
config () { config() {
return this.$store.state.config return this.$store.state.config
} }
}, },
watch: {
'$store.state.player.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
// on app mounted
mounted() {
this.setupAudio()
},
// on app destroyed
unmounted() {
this.closeAudio()
},
methods: { methods: {
on_click_outside_outputs () { on_click_outside_outputs() {
this.show_outputs_menu = false this.show_outputs_menu = false
}, },
@ -334,21 +485,24 @@ export default {
setupAudio: function () { setupAudio: function () {
const a = _audio.setupAudio() const a = _audio.setupAudio()
a.addEventListener('waiting', e => { a.addEventListener('waiting', (e) => {
this.playing = false this.playing = false
this.loading = true this.loading = true
}) })
a.addEventListener('playing', e => { a.addEventListener('playing', (e) => {
this.playing = true this.playing = true
this.loading = false this.loading = false
}) })
a.addEventListener('ended', e => { a.addEventListener('ended', (e) => {
this.playing = false this.playing = false
this.loading = false this.loading = false
}) })
a.addEventListener('error', e => { a.addEventListener('error', (e) => {
this.closeAudio() this.closeAudio()
this.$store.dispatch('add_notification', { text: 'HTTP stream error: failed to load stream or stopped loading due to network problem', type: 'danger' }) this.$store.dispatch('add_notification', {
text: 'HTTP stream error: failed to load stream or stopped loading due to network problem',
type: 'danger'
})
this.playing = false this.playing = false
this.loading = false this.loading = false
}) })
@ -385,27 +539,8 @@ export default {
this.stream_volume = newVolume this.stream_volume = newVolume
_audio.setVolume(this.stream_volume / 100) _audio.setVolume(this.stream_volume / 100)
} }
},
watch: {
'$store.state.player.volume' () {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
// on app mounted
mounted () {
this.setupAudio()
},
// on app destroyed
destroyed () {
this.closeAudio()
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,11 @@
<template> <template>
<a class="navbar-item" :class="{ 'is-active': is_active }" @click.stop.prevent="open_link()" :href="full_path()"> <a
<slot></slot> class="navbar-item"
:class="{ 'is-active': is_active }"
:href="full_path()"
@click.stop.prevent="open_link()"
>
<slot />
</a> </a>
</template> </template>
@ -15,7 +20,7 @@ export default {
}, },
computed: { computed: {
is_active () { is_active() {
if (this.exact) { if (this.exact) {
return this.$route.path === this.to return this.$route.path === this.to
} }
@ -23,19 +28,19 @@ export default {
}, },
show_player_menu: { show_player_menu: {
get () { get() {
return this.$store.state.show_player_menu return this.$store.state.show_player_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_PLAYER_MENU, value) this.$store.commit(types.SHOW_PLAYER_MENU, value)
} }
}, },
show_burger_menu: { show_burger_menu: {
get () { get() {
return this.$store.state.show_burger_menu return this.$store.state.show_burger_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_BURGER_MENU, value) this.$store.commit(types.SHOW_BURGER_MENU, value)
} }
} }

View File

@ -2,26 +2,39 @@
<div class="navbar-item"> <div class="navbar-item">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left fd-expanded"> <div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;"> <div class="level-item" style="flex-grow: 0">
<a class="button is-white is-small"> <a class="button is-white is-small">
<span class="icon fd-has-action" <span
:class="{ 'has-text-grey-light': !output.selected }" class="icon fd-has-action"
v-on:click="set_enabled"> :class="{ 'has-text-grey-light': !output.selected }"
<i class="mdi mdi-18px" :class="type_class" :title="output.type"></i> @click="set_enabled"
>
<i
class="mdi mdi-18px"
:class="type_class"
:title="output.type"
/>
</span> </span>
</a> </a>
</div> </div>
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !output.selected }">{{ output.name }}</p> <p
<Slider v-model="volume" class="heading"
:class="{ 'has-text-grey-light': !output.selected }"
>
{{ output.name }}
</p>
<Slider
v-model="volume"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
:tooltips="false" :tooltips="false"
:disabled="!output.selected" :disabled="!output.selected"
:classes="{ target: 'slider' }"
@change="set_volume" @change="set_volume"
:classes="{ target: 'slider'}" /> />
<!--range-slider <!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
@ -45,15 +58,15 @@ import webapi from '@/webapi'
export default { export default {
name: 'NavbarItemOutput', name: 'NavbarItemOutput',
components: { components: {
// RangeSlider // RangeSlider
Slider Slider
}, },
props: ['output'], props: ['output'],
computed: { computed: {
type_class () { type_class() {
if (this.output.type.startsWith('AirPlay')) { if (this.output.type.startsWith('AirPlay')) {
return 'mdi-airplay' return 'mdi-airplay'
} else if (this.output.type === 'Chromecast') { } else if (this.output.type === 'Chromecast') {
@ -65,7 +78,7 @@ export default {
} }
}, },
volume () { volume() {
return this.output.selected ? this.output.volume : 0 return this.output.selected ? this.output.volume : 0
} }
}, },
@ -89,5 +102,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,80 +1,134 @@
<template> <template>
<nav class="fd-top-navbar navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation"> <nav
class="fd-top-navbar navbar is-light is-fixed-top"
:style="zindex"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand"> <div class="navbar-brand">
<navbar-item-link to="/playlists" v-if="is_visible_playlists"> <navbar-item-link v-if="is_visible_playlists" to="/playlists">
<span class="icon"><i class="mdi mdi-library-music"></i></span> <span class="icon"><i class="mdi mdi-library-music" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/music" v-if="is_visible_music"> <navbar-item-link v-if="is_visible_music" to="/music">
<span class="icon"><i class="mdi mdi-music"></i></span> <span class="icon"><i class="mdi mdi-music" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/podcasts" v-if="is_visible_podcasts"> <navbar-item-link v-if="is_visible_podcasts" to="/podcasts">
<span class="icon"><i class="mdi mdi-microphone"></i></span> <span class="icon"><i class="mdi mdi-microphone" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/audiobooks" v-if="is_visible_audiobooks"> <navbar-item-link v-if="is_visible_audiobooks" to="/audiobooks">
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <span class="icon"><i class="mdi mdi-book-open-variant" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/radio" v-if="is_visible_radio"> <navbar-item-link v-if="is_visible_radio" to="/radio">
<span class="icon"><i class="mdi mdi-radio"></i></span> <span class="icon"><i class="mdi mdi-radio" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/files" v-if="is_visible_files"> <navbar-item-link v-if="is_visible_files" to="/files">
<span class="icon"><i class="mdi mdi-folder-open"></i></span> <span class="icon"><i class="mdi mdi-folder-open" /></span>
</navbar-item-link> </navbar-item-link>
<navbar-item-link to="/search" v-if="is_visible_search"> <navbar-item-link v-if="is_visible_search" to="/search">
<span class="icon"><i class="mdi mdi-magnify"></i></span> <span class="icon"><i class="mdi mdi-magnify" /></span>
</navbar-item-link> </navbar-item-link>
<div class="navbar-burger" @click="show_burger_menu = !show_burger_menu" :class="{ 'is-active': show_burger_menu }"> <div
<span></span> class="navbar-burger"
<span></span> :class="{ 'is-active': show_burger_menu }"
<span></span> @click="show_burger_menu = !show_burger_menu"
>
<span />
<span />
<span />
</div> </div>
</div> </div>
<div class="navbar-menu" :class="{ 'is-active': show_burger_menu }"> <div class="navbar-menu" :class="{ 'is-active': show_burger_menu }">
<div class="navbar-start"> <div class="navbar-start" />
</div>
<div class="navbar-end"> <div class="navbar-end">
<!-- Burger menu entries --> <!-- Burger menu entries -->
<div class="navbar-item has-dropdown is-hoverable" <div
:class="{ 'is-active': show_settings_menu }" class="navbar-item has-dropdown is-hoverable"
@click="on_click_outside_settings"> :class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings"
>
<a class="navbar-link is-arrowless"> <a class="navbar-link is-arrowless">
<span class="icon is-hidden-touch"><i class="mdi mdi-24px mdi-menu"></i></span> <span class="icon is-hidden-touch"
><i class="mdi mdi-24px mdi-menu"
/></span>
<span class="is-hidden-desktop has-text-weight-bold">OwnTone</span> <span class="is-hidden-desktop has-text-weight-bold">OwnTone</span>
</a> </a>
<div class="navbar-dropdown is-right"> <div class="navbar-dropdown is-right">
<navbar-item-link to="/playlists">
<span class="icon"><i class="mdi mdi-library-music" /></span>
<b>Playlists</b>
</navbar-item-link>
<navbar-item-link to="/music" exact>
<span class="icon"><i class="mdi mdi-music" /></span>
<b>Music</b>
</navbar-item-link>
<navbar-item-link to="/music/artists">
<span class="fd-navbar-item-level2">Artists</span>
</navbar-item-link>
<navbar-item-link to="/music/albums">
<span class="fd-navbar-item-level2">Albums</span>
</navbar-item-link>
<navbar-item-link to="/music/genres">
<span class="fd-navbar-item-level2">Genres</span>
</navbar-item-link>
<navbar-item-link v-if="spotify_enabled" to="/music/spotify">
<span class="fd-navbar-item-level2">Spotify</span>
</navbar-item-link>
<navbar-item-link to="/podcasts">
<span class="icon"><i class="mdi mdi-microphone" /></span>
<b>Podcasts</b>
</navbar-item-link>
<navbar-item-link to="/audiobooks">
<span class="icon"><i class="mdi mdi-book-open-variant" /></span>
<b>Audiobooks</b>
</navbar-item-link>
<navbar-item-link to="/radio">
<span class="icon"><i class="mdi mdi-radio" /></span>
<b>Radio</b>
</navbar-item-link>
<navbar-item-link to="/files">
<span class="icon"><i class="mdi mdi-folder-open" /></span>
<b>Files</b>
</navbar-item-link>
<navbar-item-link to="/search">
<span class="icon"><i class="mdi mdi-magnify" /></span>
<b>Search</b>
</navbar-item-link>
<hr class="fd-navbar-divider" />
<navbar-item-link to="/playlists"><span class="icon"><i class="mdi mdi-library-music"></i></span> <b>Playlists</b></navbar-item-link> <navbar-item-link to="/settings/webinterface">
<navbar-item-link to="/music" exact><span class="icon"><i class="mdi mdi-music"></i></span> <b>Music</b></navbar-item-link> Settings
<navbar-item-link to="/music/artists"><span class="fd-navbar-item-level2">Artists</span></navbar-item-link> </navbar-item-link>
<navbar-item-link to="/music/albums"><span class="fd-navbar-item-level2">Albums</span></navbar-item-link> <a
<navbar-item-link to="/music/genres"><span class="fd-navbar-item-level2">Genres</span></navbar-item-link> class="navbar-item"
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link> @click.stop.prevent="
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link> show_update_dialog = true
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link> show_settings_menu = false
<navbar-item-link to="/radio"><span class="icon"><i class="mdi mdi-radio"></i></span> <b>Radio</b></navbar-item-link> show_burger_menu = false
<navbar-item-link to="/files"><span class="icon"><i class="mdi mdi-folder-open"></i></span> <b>Files</b></navbar-item-link> "
<navbar-item-link to="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link> >
<hr class="fd-navbar-divider">
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
<a class="navbar-item" @click.stop.prevent="show_update_dialog = true; show_settings_menu = false; show_burger_menu = false">
Update Library Update Library
</a> </a>
<navbar-item-link to="/about">About</navbar-item-link> <navbar-item-link to="/about"> About </navbar-item-link>
<div class="navbar-item is-hidden-desktop" style="margin-bottom: 2.5rem;"></div> <div
class="navbar-item is-hidden-desktop"
style="margin-bottom: 2.5rem"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="is-overlay" v-show="show_settings_menu" <div
style="z-index:10; width: 100vw; height:100vh;" v-show="show_settings_menu"
@click="show_settings_menu = false"></div> class="is-overlay"
style="z-index: 10; width: 100vw; height: 100vh"
@click="show_settings_menu = false"
/>
</nav> </nav>
</template> </template>
@ -86,82 +140,103 @@ export default {
name: 'NavbarTop', name: 'NavbarTop',
components: { NavbarItemLink }, components: { NavbarItemLink },
data () { data() {
return { return {
show_settings_menu: false show_settings_menu: false
} }
}, },
computed: { computed: {
is_visible_playlists () { is_visible_playlists() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_playlists').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_playlists'
).value
}, },
is_visible_music () { is_visible_music() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_music').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_music'
).value
}, },
is_visible_podcasts () { is_visible_podcasts() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_podcasts').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_podcasts'
).value
}, },
is_visible_audiobooks () { is_visible_audiobooks() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_audiobooks').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_audiobooks'
).value
}, },
is_visible_radio () { is_visible_radio() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_radio').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_radio'
).value
}, },
is_visible_files () { is_visible_files() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_files').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_files'
).value
}, },
is_visible_search () { is_visible_search() {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_search').value return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_search'
).value
}, },
player () { player() {
return this.$store.state.player return this.$store.state.player
}, },
config () { config() {
return this.$store.state.config return this.$store.state.config
}, },
library () { library() {
return this.$store.state.library return this.$store.state.library
}, },
audiobooks () { audiobooks() {
return this.$store.state.audiobooks_count return this.$store.state.audiobooks_count
}, },
podcasts () { podcasts() {
return this.$store.state.podcasts_count return this.$store.state.podcasts_count
}, },
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
}, },
show_burger_menu: { show_burger_menu: {
get () { get() {
return this.$store.state.show_burger_menu return this.$store.state.show_burger_menu
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_BURGER_MENU, value) this.$store.commit(types.SHOW_BURGER_MENU, value)
} }
}, },
show_player_menu () { show_player_menu() {
return this.$store.state.show_player_menu return this.$store.state.show_player_menu
}, },
show_update_dialog: { show_update_dialog: {
get () { get() {
return this.$store.state.show_update_dialog return this.$store.state.show_update_dialog
}, },
set (value) { set(value) {
this.$store.commit(types.SHOW_UPDATE_DIALOG, value) this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
} }
}, },
zindex () { zindex() {
if (this.show_player_menu) { if (this.show_player_menu) {
return 'z-index: 20' return 'z-index: 20'
} }
@ -169,19 +244,18 @@ export default {
} }
}, },
methods: { watch: {
on_click_outside_settings () { $route(to, from) {
this.show_settings_menu = !this.show_settings_menu this.show_settings_menu = false
} }
}, },
watch: { methods: {
$route (to, from) { on_click_outside_settings() {
this.show_settings_menu = false this.show_settings_menu = !this.show_settings_menu
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,9 +1,17 @@
<template> <template>
<section class="fd-notifications" v-if="notifications.length > 0"> <section v-if="notifications.length > 0" class="fd-notifications">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-half"> <div class="column is-half">
<div class="notification has-shadow " v-for="notification in notifications" :key="notification.id" :class="['notification', notification.type ? `is-${notification.type}` : '']"> <div
<button class="delete" v-on:click="remove(notification)"></button> v-for="notification in notifications"
:key="notification.id"
class="notification has-shadow"
:class="[
'notification',
notification.type ? `is-${notification.type}` : ''
]"
>
<button class="delete" @click="remove(notification)" />
{{ notification.text }} {{ notification.text }}
</div> </div>
</div> </div>
@ -16,14 +24,14 @@ import * as types from '@/store/mutation_types'
export default { export default {
name: 'Notifications', name: 'Notifications',
components: { }, components: {},
data () { data() {
return { showNav: false } return { showNav: false }
}, },
computed: { computed: {
notifications () { notifications() {
return this.$store.state.notifications.list return this.$store.state.notifications.list
} }
}, },

View File

@ -1,6 +1,6 @@
<template> <template>
<a @click="toggle_consume_mode" :class="{ 'is-warning': is_consume }"> <a :class="{ 'is-warning': is_consume }" @click="toggle_consume_mode">
<span class="icon"><i class="mdi mdi-fire" :class="icon_style"></i></span> <span class="icon"><i class="mdi mdi-fire" :class="icon_style" /></span>
</a> </a>
</template> </template>
@ -15,7 +15,7 @@ export default {
}, },
computed: { computed: {
is_consume () { is_consume() {
return this.$store.state.player.consume return this.$store.state.player.consume
} }
}, },
@ -28,5 +28,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,8 @@
<template> <template>
<a @click="play_next" :disabled="disabled"> <a :disabled="disabled" @click="play_next">
<span class="icon"><i class="mdi mdi-skip-forward" :class="icon_style"></i></span> <span class="icon"
><i class="mdi mdi-skip-forward" :class="icon_style"
/></span>
</a> </a>
</template> </template>
@ -15,7 +17,7 @@ export default {
}, },
computed: { computed: {
disabled () { disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0 return !this.$store.state.queue || this.$store.state.queue.count <= 0
} }
}, },
@ -32,5 +34,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,17 @@
<template> <template>
<a @click="toggle_play_pause" :disabled="disabled"> <a :disabled="disabled" @click="toggle_play_pause">
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing && is_pause_allowed, 'mdi-stop': is_playing && !is_pause_allowed }]"></i></span> <span class="icon"
><i
class="mdi"
:class="[
icon_style,
{
'mdi-play': !is_playing,
'mdi-pause': is_playing && is_pause_allowed,
'mdi-stop': is_playing && !is_pause_allowed
}
]"
/></span>
</a> </a>
</template> </template>
@ -16,16 +27,18 @@ export default {
}, },
computed: { computed: {
is_playing () { is_playing() {
return this.$store.state.player.state === 'play' return this.$store.state.player.state === 'play'
}, },
is_pause_allowed () { is_pause_allowed() {
return (this.$store.getters.now_playing && return (
this.$store.getters.now_playing.data_kind !== 'pipe') this.$store.getters.now_playing &&
this.$store.getters.now_playing.data_kind !== 'pipe'
)
}, },
disabled () { disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0 return !this.$store.state.queue || this.$store.state.queue.count <= 0
} }
}, },
@ -34,7 +47,12 @@ export default {
toggle_play_pause: function () { toggle_play_pause: function () {
if (this.disabled) { if (this.disabled) {
if (this.show_disabled_message) { if (this.show_disabled_message) {
this.$store.dispatch('add_notification', { text: 'Queue is empty', type: 'info', topic: 'connection', timeout: 2000 }) this.$store.dispatch('add_notification', {
text: 'Queue is empty',
type: 'info',
topic: 'connection',
timeout: 2000
})
} }
return return
} }
@ -51,5 +69,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,8 @@
<template> <template>
<a @click="play_previous" :disabled="disabled"> <a :disabled="disabled" @click="play_previous">
<span class="icon"><i class="mdi mdi-skip-backward" :class="icon_style"></i></span> <span class="icon"
><i class="mdi mdi-skip-backward" :class="icon_style"
/></span>
</a> </a>
</template> </template>
@ -15,7 +17,7 @@ export default {
}, },
computed: { computed: {
disabled () { disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0 return !this.$store.state.queue || this.$store.state.queue.count <= 0
} }
}, },
@ -32,5 +34,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,17 @@
<template> <template>
<a @click="toggle_repeat_mode" :class="{ 'is-warning': !is_repeat_off }"> <a :class="{ 'is-warning': !is_repeat_off }" @click="toggle_repeat_mode">
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-repeat': is_repeat_all, 'mdi-repeat-once': is_repeat_single, 'mdi-repeat-off': is_repeat_off }]"></i></span> <span class="icon"
><i
class="mdi"
:class="[
icon_style,
{
'mdi-repeat': is_repeat_all,
'mdi-repeat-once': is_repeat_single,
'mdi-repeat-off': is_repeat_off
}
]"
/></span>
</a> </a>
</template> </template>
@ -15,13 +26,13 @@ export default {
}, },
computed: { computed: {
is_repeat_all () { is_repeat_all() {
return this.$store.state.player.repeat === 'all' return this.$store.state.player.repeat === 'all'
}, },
is_repeat_single () { is_repeat_single() {
return this.$store.state.player.repeat === 'single' return this.$store.state.player.repeat === 'single'
}, },
is_repeat_off () { is_repeat_off() {
return !this.is_repeat_all && !this.is_repeat_single return !this.is_repeat_all && !this.is_repeat_single
} }
}, },
@ -40,5 +51,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<a @click="seek" :disabled="disabled" v-if="visible"> <a v-if="visible" :disabled="disabled" @click="seek">
<span class="icon"><i class="mdi mdi-rewind" :class="icon_style"></i></span> <span class="icon"><i class="mdi mdi-rewind" :class="icon_style" /></span>
</a> </a>
</template> </template>
@ -12,17 +12,21 @@ export default {
props: ['seek_ms', 'icon_style'], props: ['seek_ms', 'icon_style'],
computed: { computed: {
now_playing () { now_playing() {
return this.$store.getters.now_playing return this.$store.getters.now_playing
}, },
is_stopped () { is_stopped() {
return this.$store.state.player.state === 'stop' return this.$store.state.player.state === 'stop'
}, },
disabled () { disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped || return (
this.now_playing.data_kind === 'pipe' !this.$store.state.queue ||
this.$store.state.queue.count <= 0 ||
this.is_stopped ||
this.now_playing.data_kind === 'pipe'
)
}, },
visible () { visible() {
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind) return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
} }
}, },

View File

@ -1,6 +1,8 @@
<template> <template>
<a @click="seek" :disabled="disabled" v-if="visible"> <a v-if="visible" :disabled="disabled" @click="seek">
<span class="icon"><i class="mdi mdi-fast-forward" :class="icon_style"></i></span> <span class="icon"
><i class="mdi mdi-fast-forward" :class="icon_style"
/></span>
</a> </a>
</template> </template>
@ -12,17 +14,21 @@ export default {
props: ['seek_ms', 'icon_style'], props: ['seek_ms', 'icon_style'],
computed: { computed: {
now_playing () { now_playing() {
return this.$store.getters.now_playing return this.$store.getters.now_playing
}, },
is_stopped () { is_stopped() {
return this.$store.state.player.state === 'stop' return this.$store.state.player.state === 'stop'
}, },
disabled () { disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped || return (
this.now_playing.data_kind === 'pipe' !this.$store.state.queue ||
this.$store.state.queue.count <= 0 ||
this.is_stopped ||
this.now_playing.data_kind === 'pipe'
)
}, },
visible () { visible() {
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind) return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
} }
}, },

View File

@ -1,6 +1,13 @@
<template> <template>
<a @click="toggle_shuffle_mode" :class="{ 'is-warning': is_shuffle }"> <a :class="{ 'is-warning': is_shuffle }" @click="toggle_shuffle_mode">
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-shuffle': is_shuffle, 'mdi-shuffle-disabled': !is_shuffle }]"></i></span> <span class="icon"
><i
class="mdi"
:class="[
icon_style,
{ 'mdi-shuffle': is_shuffle, 'mdi-shuffle-disabled': !is_shuffle }
]"
/></span>
</a> </a>
</template> </template>
@ -15,7 +22,7 @@ export default {
}, },
computed: { computed: {
is_shuffle () { is_shuffle() {
return this.$store.state.player.shuffle return this.$store.state.player.shuffle
} }
}, },
@ -28,5 +35,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,5 +1,9 @@
<template> <template>
<div v-if="width > 0" class="progress-bar mt-2" :style="{ width: width_percent }" /> <div
v-if="width > 0"
class="progress-bar mt-2"
:style="{ width: width_percent }"
/>
</template> </template>
<script> <script>
@ -8,13 +12,13 @@ export default {
props: ['max', 'value'], props: ['max', 'value'],
computed: { computed: {
width () { width() {
if (this.value > 0 && this.max > 0) { if (this.value > 0 && this.max > 0) {
return parseInt(this.value * 100 / this.max) return parseInt((this.value * 100) / this.max)
} }
return 0 return 0
}, },
width_percent () { width_percent() {
return this.width + '%' return this.width + '%'
} }
} }

View File

@ -1,19 +1,25 @@
<template> <template>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" <input
:checked="value" ref="settings_checkbox"
@change="set_update_timer" type="checkbox"
ref="settings_checkbox"> :checked="value"
<slot name="label"></slot> @change="set_update_timer"
<i class="is-size-7" />
:class="{ <slot name="label" />
'has-text-info': statusUpdate === 'success', <i
'has-text-danger': statusUpdate === 'error' class="is-size-7"
}"> {{ info }}</i> :class="{
'has-text-info': statusUpdate === 'success',
'has-text-danger': statusUpdate === 'error'
}"
>
{{ info }}</i
>
</label> </label>
<p class="help" v-if="$slots['info']"> <p v-if="$slots['info']" class="help">
<slot name="info"></slot> <slot name="info" />
</p> </p>
</div> </div>
</template> </template>
@ -27,7 +33,7 @@ export default {
props: ['category_name', 'option_name'], props: ['category_name', 'option_name'],
data () { data() {
return { return {
timerDelay: 2000, timerDelay: 2000,
timerId: -1, timerId: -1,
@ -38,22 +44,26 @@ export default {
}, },
computed: { computed: {
category () { category() {
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name) return this.$store.state.settings.categories.find(
(elem) => elem.name === this.category_name
)
}, },
option () { option() {
if (!this.category) { if (!this.category) {
return {} return {}
} }
return this.category.options.find(elem => elem.name === this.option_name) return this.category.options.find(
(elem) => elem.name === this.option_name
)
}, },
value () { value() {
return this.option.value return this.option.value
}, },
info () { info() {
if (this.statusUpdate === 'success') { if (this.statusUpdate === 'success') {
return '(setting saved)' return '(setting saved)'
} else if (this.statusUpdate === 'error') { } else if (this.statusUpdate === 'error') {
@ -64,7 +74,7 @@ export default {
}, },
methods: { methods: {
set_update_timer () { set_update_timer() {
if (this.timerId > 0) { if (this.timerId > 0) {
window.clearTimeout(this.timerId) window.clearTimeout(this.timerId)
this.timerId = -1 this.timerId = -1
@ -77,7 +87,7 @@ export default {
} }
}, },
update_setting () { update_setting() {
this.timerId = -1 this.timerId = -1
const newValue = this.$refs.settings_checkbox.checked const newValue = this.$refs.settings_checkbox.checked
@ -92,15 +102,19 @@ export default {
name: this.option_name, name: this.option_name,
value: newValue value: newValue
} }
webapi.settings_update(this.category.name, option).then(() => { webapi
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) .settings_update(this.category.name, option)
this.statusUpdate = 'success' .then(() => {
}).catch(() => { this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'error' this.statusUpdate = 'success'
this.$refs.settings_checkbox.checked = this.value })
}).finally(() => { .catch(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.statusUpdate = 'error'
}) this.$refs.settings_checkbox.checked = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}, },
clear_status: function () { clear_status: function () {
@ -110,5 +124,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,25 +2,31 @@
<fieldset :disabled="disabled"> <fieldset :disabled="disabled">
<div class="field"> <div class="field">
<label class="label has-text-weight-normal"> <label class="label has-text-weight-normal">
<slot name="label"></slot> <slot name="label" />
<i class="is-size-7" <i
:class="{ class="is-size-7"
'has-text-info': statusUpdate === 'success', :class="{
'has-text-danger': statusUpdate === 'error' 'has-text-info': statusUpdate === 'success',
}"> {{ info }}</i> 'has-text-danger': statusUpdate === 'error'
}"
>
{{ info }}</i
>
</label> </label>
<div class="control"> <div class="control">
<input class="input" <input
type="number" ref="settings_number"
min="0" class="input"
style="width: 10em;" type="number"
:placeholder="placeholder" min="0"
:value="value" style="width: 10em"
@input="set_update_timer" :placeholder="placeholder"
ref="settings_number"> :value="value"
@input="set_update_timer"
/>
</div> </div>
<p class="help" v-if="$slots['info']"> <p v-if="$slots['info']" class="help">
<slot name="info"></slot> <slot name="info" />
</p> </p>
</div> </div>
</fieldset> </fieldset>
@ -35,7 +41,7 @@ export default {
props: ['category_name', 'option_name', 'placeholder', 'disabled'], props: ['category_name', 'option_name', 'placeholder', 'disabled'],
data () { data() {
return { return {
timerDelay: 2000, timerDelay: 2000,
timerId: -1, timerId: -1,
@ -45,22 +51,26 @@ export default {
}, },
computed: { computed: {
category () { category() {
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name) return this.$store.state.settings.categories.find(
(elem) => elem.name === this.category_name
)
}, },
option () { option() {
if (!this.category) { if (!this.category) {
return {} return {}
} }
return this.category.options.find(elem => elem.name === this.option_name) return this.category.options.find(
(elem) => elem.name === this.option_name
)
}, },
value () { value() {
return this.option.value return this.option.value
}, },
info () { info() {
if (this.statusUpdate === 'success') { if (this.statusUpdate === 'success') {
return '(setting saved)' return '(setting saved)'
} else if (this.statusUpdate === 'error') { } else if (this.statusUpdate === 'error') {
@ -71,7 +81,7 @@ export default {
}, },
methods: { methods: {
set_update_timer () { set_update_timer() {
if (this.timerId > 0) { if (this.timerId > 0) {
window.clearTimeout(this.timerId) window.clearTimeout(this.timerId)
this.timerId = -1 this.timerId = -1
@ -84,7 +94,7 @@ export default {
} }
}, },
update_setting () { update_setting() {
this.timerId = -1 this.timerId = -1
const newValue = this.$refs.settings_number.value const newValue = this.$refs.settings_number.value
@ -98,15 +108,19 @@ export default {
name: this.option_name, name: this.option_name,
value: parseInt(newValue, 10) value: parseInt(newValue, 10)
} }
webapi.settings_update(this.category.name, option).then(() => { webapi
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) .settings_update(this.category.name, option)
this.statusUpdate = 'success' .then(() => {
}).catch(() => { this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'error' this.statusUpdate = 'success'
this.$refs.settings_number.value = this.value })
}).finally(() => { .catch(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.statusUpdate = 'error'
}) this.$refs.settings_number.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}, },
clear_status: function () { clear_status: function () {
@ -116,5 +130,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,21 +2,29 @@
<fieldset :disabled="disabled"> <fieldset :disabled="disabled">
<div class="field"> <div class="field">
<label class="label has-text-weight-normal"> <label class="label has-text-weight-normal">
<slot name="label"></slot> <slot name="label" />
<i class="is-size-7" <i
:class="{ class="is-size-7"
'has-text-info': statusUpdate === 'success', :class="{
'has-text-danger': statusUpdate === 'error' 'has-text-info': statusUpdate === 'success',
}"> {{ info }}</i> 'has-text-danger': statusUpdate === 'error'
}"
>
{{ info }}</i
>
</label> </label>
<div class="control"> <div class="control">
<input class="input" type="text" :placeholder="placeholder" <input
:value="value" ref="settings_text"
@input="set_update_timer" class="input"
ref="settings_text"> type="text"
:placeholder="placeholder"
:value="value"
@input="set_update_timer"
/>
</div> </div>
<p class="help" v-if="$slots['info']"> <p v-if="$slots['info']" class="help">
<slot name="info"></slot> <slot name="info" />
</p> </p>
</div> </div>
</fieldset> </fieldset>
@ -31,7 +39,7 @@ export default {
props: ['category_name', 'option_name', 'placeholder', 'disabled'], props: ['category_name', 'option_name', 'placeholder', 'disabled'],
data () { data() {
return { return {
timerDelay: 2000, timerDelay: 2000,
timerId: -1, timerId: -1,
@ -42,22 +50,26 @@ export default {
}, },
computed: { computed: {
category () { category() {
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name) return this.$store.state.settings.categories.find(
(elem) => elem.name === this.category_name
)
}, },
option () { option() {
if (!this.category) { if (!this.category) {
return {} return {}
} }
return this.category.options.find(elem => elem.name === this.option_name) return this.category.options.find(
(elem) => elem.name === this.option_name
)
}, },
value () { value() {
return this.option.value return this.option.value
}, },
info () { info() {
if (this.statusUpdate === 'success') { if (this.statusUpdate === 'success') {
return '(setting saved)' return '(setting saved)'
} else if (this.statusUpdate === 'error') { } else if (this.statusUpdate === 'error') {
@ -68,7 +80,7 @@ export default {
}, },
methods: { methods: {
set_update_timer () { set_update_timer() {
if (this.timerId > 0) { if (this.timerId > 0) {
window.clearTimeout(this.timerId) window.clearTimeout(this.timerId)
this.timerId = -1 this.timerId = -1
@ -81,7 +93,7 @@ export default {
} }
}, },
update_setting () { update_setting() {
this.timerId = -1 this.timerId = -1
const newValue = this.$refs.settings_text.value const newValue = this.$refs.settings_text.value
@ -95,15 +107,19 @@ export default {
name: this.option_name, name: this.option_name,
value: newValue value: newValue
} }
webapi.settings_update(this.category.name, option).then(() => { webapi
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) .settings_update(this.category.name, option)
this.statusUpdate = 'success' .then(() => {
}).catch(() => { this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'error' this.statusUpdate = 'success'
this.$refs.settings_text.value = this.value })
}).finally(() => { .catch(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.statusUpdate = 'error'
}) this.$refs.settings_text.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}, },
clear_status: function () { clear_status: function () {
@ -113,5 +129,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,16 +1,21 @@
<template> <template>
<div class="media"> <div class="media">
<div class="media-left fd-has-action" <div v-if="$slots['artwork']" class="media-left fd-has-action">
v-if="$slots['artwork']"> <slot name="artwork" />
<slot name="artwork"></slot>
</div> </div>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ album.name }}</h1> <h1 class="title is-6">
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2> {{ album.name }}
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ $filters.time(album.release_date, 'L') }})</h2> </h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ album.artists[0].name }}</b>
</h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">
({{ album.album_type }}, {{ $filters.time(album.release_date, 'L') }})
</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -22,5 +27,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,10 +1,12 @@
<template> <template>
<div class="media"> <div class="media">
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist"> <div class="media-content fd-has-action is-clipped" @click="open_artist">
<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">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -22,5 +24,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,11 +1,15 @@
<template> <template>
<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="open_playlist">
<h1 class="title is-6">{{ playlist.name }}</h1> <h1 class="title is-6">
<h2 class="subtitle is-7">{{ playlist.owner.display_name }}</h2> {{ playlist.name }}
</h1>
<h2 class="subtitle is-7">
{{ playlist.owner.display_name }}
</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -17,11 +21,12 @@ export default {
methods: { methods: {
open_playlist: function () { open_playlist: function () {
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id }) this.$router.push({
path: '/music/spotify/playlists/' + this.playlist.id
})
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,14 +1,30 @@
<template> <template>
<div class="media"> <div class="media">
<div class="media-content fd-has-action is-clipped" v-on:click="play"> <div class="media-content fd-has-action is-clipped" @click="play">
<h1 class="title is-6" :class="{ 'has-text-grey-light': track.is_playable === false }">{{ track.name }}</h1> <h1
<h2 class="subtitle is-7" :class="{ 'has-text-grey': track.is_playable, 'has-text-grey-light': track.is_playable === false }"><b>{{ track.artists[0].name }}</b></h2> class="title is-6"
<h2 class="subtitle is-7" v-if="track.is_playable === false"> :class="{ 'has-text-grey-light': track.is_playable === false }"
(Track is not playable<span v-if="track.restrictions && track.restrictions.reason">, restriction reason: {{ track.restrictions.reason }}</span>) >
{{ track.name }}
</h1>
<h2
class="subtitle is-7"
:class="{
'has-text-grey': track.is_playable,
'has-text-grey-light': track.is_playable === false
}"
>
<b>{{ track.artists[0].name }}</b>
</h2>
<h2 v-if="track.is_playable === false" class="subtitle is-7">
(Track is not playable<span
v-if="track.restrictions && track.restrictions.reason"
>, restriction reason: {{ track.restrictions.reason }}</span
>)
</h2> </h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
</template> </template>
@ -29,5 +45,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,25 +1,39 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<figure class="image is-square fd-has-margin-bottom" v-show="artwork_visible"> <figure
<img :src="artwork_url" @load="artwork_loaded" @error="artwork_error" class="fd-has-shadow"> v-show="artwork_visible"
class="image is-square fd-has-margin-bottom"
>
<img
:src="artwork_url"
class="fd-has-shadow"
@load="artwork_loaded"
@error="artwork_error"
/>
</figure> </figure>
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_album">{{ album.name }}</a> <a class="has-text-link" @click="open_album">{{
album.name
}}</a>
</p> </p>
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Album artist</span> <span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a> <a class="title is-6 has-text-link" @click="open_artist">{{
album.artists[0].name
}}</a>
</p> </p>
<p> <p>
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ $filters.time(album.release_date, 'L') }}</span> <span class="title is-6">{{
$filters.time(album.release_date, 'L')
}}</span>
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>
@ -29,18 +43,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -53,7 +74,7 @@ export default {
name: 'SpotifyModalDialogAlbum', name: 'SpotifyModalDialogAlbum',
props: ['show', 'album'], props: ['show', 'album'],
data () { data() {
return { return {
artwork_visible: false artwork_visible: false
} }
@ -89,7 +110,9 @@ export default {
}, },
open_artist: function () { open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id }) this.$router.push({
path: '/music/spotify/artists/' + this.album.artists[0].id
})
}, },
artwork_loaded: function () { artwork_loaded: function () {
@ -103,5 +126,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,18 +1,23 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a> <a class="has-text-link" @click="open_artist">{{
artist.name
}}</a>
</p> </p>
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Popularity / Followers</span> <span class="heading">Popularity / Followers</span>
<span class="title is-6">{{ artist.popularity }} / {{ artist.followers.total }}</span> <span class="title is-6"
>{{ artist.popularity }} /
{{ artist.followers.total }}</span
>
</p> </p>
<p> <p>
<span class="heading">Genres</span> <span class="heading">Genres</span>
@ -22,18 +27,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -69,5 +81,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,18 +1,22 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<p class="title is-4"> <p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a> <a class="has-text-link" @click="open_playlist">{{
playlist.name
}}</a>
</p> </p>
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Owner</span> <span class="heading">Owner</span>
<span class="title is-6">{{ playlist.owner.display_name }}</span> <span class="title is-6">{{
playlist.owner.display_name
}}</span>
</p> </p>
<p> <p>
<span class="heading">Tracks</span> <span class="heading">Tracks</span>
@ -26,18 +30,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -67,11 +78,12 @@ export default {
}, },
open_playlist: function () { open_playlist: function () {
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id }) this.$router.push({
path: '/music/spotify/playlists/' + this.playlist.id
})
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<transition name="fade"> <transition name="fade">
<div class="modal is-active" v-if="show"> <div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')"></div> <div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card"> <div class="modal-content fd-modal-card">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
@ -15,23 +15,33 @@
<div class="content is-small"> <div class="content is-small">
<p> <p>
<span class="heading">Album</span> <span class="heading">Album</span>
<a class="title is-6 has-text-link" @click="open_album">{{ album.name }}</a> <a class="title is-6 has-text-link" @click="open_album">{{
album.name
}}</a>
</p> </p>
<p> <p>
<span class="heading">Album artist</span> <span class="heading">Album artist</span>
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artists[0].name }}</a> <a class="title is-6 has-text-link" @click="open_artist">{{
album.artists[0].name
}}</a>
</p> </p>
<p> <p>
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ $filters.time(album.release_date, 'L') }}</span> <span class="title is-6">{{
$filters.time(album.release_date, 'L')
}}</span>
</p> </p>
<p> <p>
<span class="heading">Track / Disc</span> <span class="heading">Track / Disc</span>
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span> <span class="title is-6"
>{{ track.track_number }} / {{ track.disc_number }}</span
>
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ $filters.duration(track.duration_ms) }}</span> <span class="title is-6">{{
$filters.duration(track.duration_ms)
}}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>
@ -41,18 +51,25 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add"> <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> <span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="queue_add_next"> <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> <span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a> </a>
<a class="card-footer-item has-text-dark" @click="play"> <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> <span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a> </a>
</footer> </footer>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button> <button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -86,11 +103,12 @@ export default {
}, },
open_artist: function () { open_artist: function () {
this.$router.push({ path: '/music/spotify/artists/' + this.album.artists[0].id }) this.$router.push({
path: '/music/spotify/artists/' + this.album.artists[0].id
})
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -5,18 +5,30 @@
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="tabs is-centered is-small"> <div class="tabs is-centered is-small">
<ul> <ul>
<router-link to="/audiobooks/artists" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/audiobooks/artists"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-artist"></i></span> <span class="icon is-small"
><i class="mdi mdi-artist"
/></span>
<span class="">Authors</span> <span class="">Authors</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/audiobooks/albums" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/audiobooks/albums"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-album"></i></span> <span class="icon is-small"
><i class="mdi mdi-album"
/></span>
<span class="">Audiobooks</span> <span class="">Audiobooks</span>
</a> </a>
</li> </li>
@ -35,5 +47,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -5,50 +5,85 @@
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="tabs is-centered is-small"> <div class="tabs is-centered is-small">
<ul> <ul>
<router-link to="/music/browse" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/music/browse"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-web"></i></span> <span class="icon is-small"><i class="mdi mdi-web" /></span>
<span class="">Browse</span> <span class="">Browse</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/music/artists" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/music/artists"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-artist"></i></span> <span class="icon is-small"
><i class="mdi mdi-artist"
/></span>
<span class="">Artists</span> <span class="">Artists</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/music/albums" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/music/albums"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-album"></i></span> <span class="icon is-small"
><i class="mdi mdi-album"
/></span>
<span class="">Albums</span> <span class="">Albums</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/music/genres" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/music/genres"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-speaker"></i></span> <span class="icon is-small"
><i class="mdi mdi-speaker"
/></span>
<span class="">Genres</span> <span class="">Genres</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/music/composers" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/music/composers"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-book-open-page-variant"></i></span> <span class="icon is-small"
><i class="mdi mdi-book-open-page-variant"
/></span>
<span class="">Composers</span> <span class="">Composers</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/music/spotify" v-if="spotify_enabled" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-if="spotify_enabled"
v-slot="{ navigate, isActive }"
to="/music/spotify"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span> <span class="icon is-small"
><i class="mdi mdi-spotify"
/></span>
<span class="">Spotify</span> <span class="">Spotify</span>
</a> </a>
</li> </li>
@ -66,12 +101,11 @@ export default {
name: 'TabsMusic', name: 'TabsMusic',
computed: { computed: {
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<section class="section fd-remove-padding-bottom" v-if="spotify_enabled"> <section v-if="spotify_enabled" 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">
@ -7,13 +7,17 @@
<ul> <ul>
<li :class="{ 'is-active': $route.path === '/search/library' }"> <li :class="{ 'is-active': $route.path === '/search/library' }">
<a @click="search_library"> <a @click="search_library">
<span class="icon is-small"><i class="mdi mdi-library-books"></i></span> <span class="icon is-small"
><i class="mdi mdi-library-books"
/></span>
<span class="">Library</span> <span class="">Library</span>
</a> </a>
</li> </li>
<li :class="{ 'is-active': $route.path === '/search/spotify' }"> <li :class="{ 'is-active': $route.path === '/search/spotify' }">
<a @click="search_spotify"> <a @click="search_spotify">
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span> <span class="icon is-small"
><i class="mdi mdi-spotify"
/></span>
<span class="">Spotify</span> <span class="">Spotify</span>
</a> </a>
</li> </li>
@ -32,7 +36,7 @@ export default {
props: ['query'], props: ['query'],
computed: { computed: {
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
}, },
@ -68,5 +72,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -5,29 +5,45 @@
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="tabs is-centered is-small"> <div class="tabs is-centered is-small">
<ul> <ul>
<router-link to="/settings/webinterface" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/settings/webinterface"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="">Webinterface</span> <span class="">Webinterface</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/settings/remotes-outputs" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/settings/remotes-outputs"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="">Remotes &amp; Outputs</span> <span class="">Remotes &amp; Outputs</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/settings/artwork" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/settings/artwork"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="">Artwork</span> <span class="">Artwork</span>
</a> </a>
</li> </li>
</router-link> </router-link>
<router-link to="/settings/online-services" custom v-slot="{ navigate, isActive }"> <router-link
<li :class="{'is-active': isActive}"> v-slot="{ navigate, isActive }"
to="/settings/online-services"
custom
>
<li :class="{ 'is-active': isActive }">
<a @click="navigate" @keypress.enter="navigate"> <a @click="navigate" @keypress.enter="navigate">
<span class="">Online Services</span> <span class="">Online Services</span>
</a> </a>
@ -45,10 +61,8 @@
export default { export default {
name: 'TabsSettings', name: 'TabsSettings',
computed: { computed: {}
}
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,6 +1,13 @@
export default class Albums { export default class Albums {
constructor (items, options = { hideSingles: false, hideSpotify: false, sort: 'Name', group: false }) { constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items this.items = items
this.options = options this.options = options
this.grouped = {} this.grouped = {}
@ -10,13 +17,13 @@ export default class Albums {
this.init() this.init()
} }
init () { init() {
this.createSortedAndFilteredList() this.createSortedAndFilteredList()
this.createGroupedList() this.createGroupedList()
this.createIndexList() this.createIndexList()
} }
getAlbumIndex (album) { getAlbumIndex(album) {
if (this.options.sort === 'Recently added') { if (this.options.sort === 'Recently added') {
return album.time_added.substring(0, 4) return album.time_added.substring(0, 4)
} else if (this.options.sort === 'Recently added (browse)') { } else if (this.options.sort === 'Recently added (browse)') {
@ -29,24 +36,27 @@ export default class Albums {
return album.name_sort.charAt(0).toUpperCase() return album.name_sort.charAt(0).toUpperCase()
} }
getRecentlyAddedBrowseIndex (recentlyAdded) { getRecentlyAddedBrowseIndex(recentlyAdded) {
if (!recentlyAdded) { if (!recentlyAdded) {
return '0000' return '0000'
} }
const diff = new Date().getTime() - new Date(recentlyAdded).getTime() const diff = new Date().getTime() - new Date(recentlyAdded).getTime()
if (diff < 86400000) { // 24h if (diff < 86400000) {
// 24h
return 'Today' return 'Today'
} else if (diff < 604800000) { // 7 days } else if (diff < 604800000) {
// 7 days
return 'Last week' return 'Last week'
} else if (diff < 2592000000) { // 30 days } else if (diff < 2592000000) {
// 30 days
return 'Last month' return 'Last month'
} }
return recentlyAdded.substring(0, 4) return recentlyAdded.substring(0, 4)
} }
isAlbumVisible (album) { isAlbumVisible(album) {
if (this.options.hideSingles && album.track_count <= 2) { if (this.options.hideSingles && album.track_count <= 2) {
return false return false
} }
@ -56,18 +66,30 @@ export default class Albums {
return true return true
} }
createIndexList () { createIndexList() {
this.indexList = [...new Set(this.sortedAndFiltered this.indexList = [
.map(album => this.getAlbumIndex(album)))] ...new Set(
this.sortedAndFiltered.map((album) => this.getAlbumIndex(album))
)
]
} }
createSortedAndFilteredList () { createSortedAndFilteredList() {
let albumsSorted = this.items let albumsSorted = this.items
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) { if (
albumsSorted = albumsSorted.filter(album => this.isAlbumVisible(album)) this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
albumsSorted = albumsSorted.filter((album) => this.isAlbumVisible(album))
} }
if (this.options.sort === 'Recently added' || this.options.sort === 'Recently added (browse)') { if (
albumsSorted = [...albumsSorted].sort((a, b) => b.time_added.localeCompare(a.time_added)) this.options.sort === 'Recently added' ||
this.options.sort === 'Recently added (browse)'
) {
albumsSorted = [...albumsSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
} else if (this.options.sort === 'Recently released') { } else if (this.options.sort === 'Recently released') {
albumsSorted = [...albumsSorted].sort((a, b) => { albumsSorted = [...albumsSorted].sort((a, b) => {
if (!a.date_released) { if (!a.date_released) {
@ -92,13 +114,13 @@ export default class Albums {
this.sortedAndFiltered = albumsSorted this.sortedAndFiltered = albumsSorted
} }
createGroupedList () { createGroupedList() {
if (!this.options.group) { if (!this.options.group) {
this.grouped = {} this.grouped = {}
} }
this.grouped = this.sortedAndFiltered.reduce((r, album) => { this.grouped = this.sortedAndFiltered.reduce((r, album) => {
const idx = this.getAlbumIndex(album) const idx = this.getAlbumIndex(album)
r[idx] = [...r[idx] || [], album] r[idx] = [...(r[idx] || []), album]
return r return r
}, {}) }, {})
} }

View File

@ -1,6 +1,13 @@
export default class Artists { export default class Artists {
constructor (items, options = { hideSingles: false, hideSpotify: false, sort: 'Name', group: false }) { constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items this.items = items
this.options = options this.options = options
this.grouped = {} this.grouped = {}
@ -10,21 +17,24 @@ export default class Artists {
this.init() this.init()
} }
init () { init() {
this.createSortedAndFilteredList() this.createSortedAndFilteredList()
this.createGroupedList() this.createGroupedList()
this.createIndexList() this.createIndexList()
} }
getArtistIndex (artist) { getArtistIndex(artist) {
if (this.options.sort === 'Name') { if (this.options.sort === 'Name') {
return artist.name_sort.charAt(0).toUpperCase() return artist.name_sort.charAt(0).toUpperCase()
} }
return artist.time_added.substring(0, 4) return artist.time_added.substring(0, 4)
} }
isArtistVisible (artist) { isArtistVisible(artist) {
if (this.options.hideSingles && artist.track_count <= (artist.album_count * 2)) { if (
this.options.hideSingles &&
artist.track_count <= artist.album_count * 2
) {
return false return false
} }
if (this.options.hideSpotify && artist.data_kind === 'spotify') { if (this.options.hideSpotify && artist.data_kind === 'spotify') {
@ -33,29 +43,40 @@ export default class Artists {
return true return true
} }
createIndexList () { createIndexList() {
this.indexList = [...new Set(this.sortedAndFiltered this.indexList = [
.map(artist => this.getArtistIndex(artist)))] ...new Set(
this.sortedAndFiltered.map((artist) => this.getArtistIndex(artist))
)
]
} }
createSortedAndFilteredList () { createSortedAndFilteredList() {
let artistsSorted = this.items let artistsSorted = this.items
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) { if (
artistsSorted = artistsSorted.filter(artist => this.isArtistVisible(artist)) this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
artistsSorted = artistsSorted.filter((artist) =>
this.isArtistVisible(artist)
)
} }
if (this.options.sort === 'Recently added') { if (this.options.sort === 'Recently added') {
artistsSorted = [...artistsSorted].sort((a, b) => b.time_added.localeCompare(a.time_added)) artistsSorted = [...artistsSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
} }
this.sortedAndFiltered = artistsSorted this.sortedAndFiltered = artistsSorted
} }
createGroupedList () { createGroupedList() {
if (!this.options.group) { if (!this.options.group) {
this.grouped = {} this.grouped = {}
} }
this.grouped = this.sortedAndFiltered.reduce((r, artist) => { this.grouped = this.sortedAndFiltered.reduce((r, artist) => {
const idx = this.getArtistIndex(artist) const idx = this.getArtistIndex(artist)
r[idx] = [...r[idx] || [], artist] r[idx] = [...(r[idx] || []), artist]
return r return r
}, {}) }, {})
} }

View File

@ -1,6 +1,13 @@
export default class Composers { export default class Composers {
constructor (items, options = { hideSingles: false, hideSpotify: false, sort: 'Name', group: false }) { constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items this.items = items
this.options = options this.options = options
this.grouped = {} this.grouped = {}
@ -10,21 +17,24 @@ export default class Composers {
this.init() this.init()
} }
init () { init() {
this.createSortedAndFilteredList() this.createSortedAndFilteredList()
this.createGroupedList() this.createGroupedList()
this.createIndexList() this.createIndexList()
} }
getComposerIndex (composer) { getComposerIndex(composer) {
if (this.options.sort === 'Name') { if (this.options.sort === 'Name') {
return composer.name_sort.charAt(0).toUpperCase() return composer.name_sort.charAt(0).toUpperCase()
} }
return composer.time_added.substring(0, 4) return composer.time_added.substring(0, 4)
} }
isComposerVisible (composer) { isComposerVisible(composer) {
if (this.options.hideSingles && composer.track_count <= (composer.album_count * 2)) { if (
this.options.hideSingles &&
composer.track_count <= composer.album_count * 2
) {
return false return false
} }
if (this.options.hideSpotify && composer.data_kind === 'spotify') { if (this.options.hideSpotify && composer.data_kind === 'spotify') {
@ -33,29 +43,42 @@ export default class Composers {
return true return true
} }
createIndexList () { createIndexList() {
this.indexList = [...new Set(this.sortedAndFiltered this.indexList = [
.map(composer => this.getComposerIndex(composer)))] ...new Set(
this.sortedAndFiltered.map((composer) =>
this.getComposerIndex(composer)
)
)
]
} }
createSortedAndFilteredList () { createSortedAndFilteredList() {
let composersSorted = this.items let composersSorted = this.items
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) { if (
composersSorted = composersSorted.filter(composer => this.isComposerVisible(composer)) this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
composersSorted = composersSorted.filter((composer) =>
this.isComposerVisible(composer)
)
} }
if (this.options.sort === 'Recently added') { if (this.options.sort === 'Recently added') {
composersSorted = [...composersSorted].sort((a, b) => b.time_added.localeCompare(a.time_added)) composersSorted = [...composersSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
} }
this.sortedAndFiltered = composersSorted this.sortedAndFiltered = composersSorted
} }
createGroupedList () { createGroupedList() {
if (!this.options.group) { if (!this.options.group) {
this.grouped = {} this.grouped = {}
} }
this.grouped = this.sortedAndFiltered.reduce((r, composer) => { this.grouped = this.sortedAndFiltered.reduce((r, composer) => {
const idx = this.getComposerIndex(composer) const idx = this.getComposerIndex(composer)
r[idx] = [...r[idx] || [], composer] r[idx] = [...(r[idx] || []), composer]
return r return r
}, {}) }, {})
} }

View File

@ -6,51 +6,67 @@
import stringToColor from 'string-to-color' import stringToColor from 'string-to-color'
function is_background_light(background_color) {
function is_background_light (background_color) {
// Based on https://stackoverflow.com/a/44615197 // Based on https://stackoverflow.com/a/44615197
const hex = background_color.replace(/#/, '') const hex = background_color.replace(/#/, '')
const r = parseInt(hex.substr(0, 2), 16) const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16) const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16) const b = parseInt(hex.substr(4, 2), 16)
const luma = [ const luma = [0.299 * r, 0.587 * g, 0.114 * b].reduce((a, b) => a + b) / 255
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255
return luma > 0.5 return luma > 0.5
} }
function calc_text_color (background_color) { function calc_text_color(background_color) {
return is_background_light(background_color) ? '#000000' : '#ffffff' return is_background_light(background_color) ? '#000000' : '#ffffff'
} }
function createSVG (data) { function createSVG(data) {
const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' + const svg =
'<defs>' + '<svg width="' +
'<style type="text/css">' + data.width +
' #holder text {' + '" height="' +
' fill: ' + data.textColor + ';' + data.height +
' font-family: ' + data.fontFamily + ';' + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' +
' font-size: ' + data.fontSize + 'px;' + data.width +
' font-weight: ' + data.fontWeight + ';' + ' ' +
' }' + data.height +
' </style>' + '" preserveAspectRatio="none">' +
'</defs>' + '<defs>' +
'<g id="holder">' + '<style type="text/css">' +
' <rect width="100%" height="100%" fill="' + data.backgroundColor + '"></rect>' + ' #holder text {' +
' <g>' + ' fill: ' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' + data.caption + '</text>' + data.textColor +
' </g>' + ';' +
'</g>' + ' font-family: ' +
'</svg>' data.fontFamily +
';' +
' font-size: ' +
data.fontSize +
'px;' +
' font-weight: ' +
data.fontWeight +
';' +
' }' +
' </style>' +
'</defs>' +
'<g id="holder">' +
' <rect width="100%" height="100%" fill="' +
data.backgroundColor +
'"></rect>' +
' <g>' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' +
data.caption +
'</text>' +
' </g>' +
'</g>' +
'</svg>'
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg) return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
} }
function renderSVG (caption, alt_text, params) { function renderSVG(caption, alt_text, params) {
const background_color = stringToColor(alt_text) const background_color = stringToColor(alt_text)
const text_color = calc_text_color(background_color) const text_color = calc_text_color(background_color)
const paramsSVG = { const paramsSVG = {
@ -66,4 +82,4 @@ function renderSVG (caption, alt_text, params) {
return createSVG(paramsSVG) return createSVG(paramsSVG)
} }
export { renderSVG } export { renderSVG }

View File

@ -2,7 +2,7 @@ import { createApp } from 'vue'
import store from './store' import store from './store'
import { router } from './router' import { router } from './router'
import VueProgressBar from '@aacassandra/vue3-progressbar' import VueProgressBar from '@aacassandra/vue3-progressbar'
import VueClickAway from "vue3-click-away" import VueClickAway from 'vue3-click-away'
import VueLazyLoad from 'vue3-lazyload' import VueLazyLoad from 'vue3-lazyload'
import VueScrollTo from 'vue-scrollto' import VueScrollTo from 'vue-scrollto'
import { filters } from './filter' import { filters } from './filter'

View File

@ -3,7 +3,6 @@
@import 'bulma/bulma.sass'; @import 'bulma/bulma.sass';
@import 'bulma-switch'; @import 'bulma-switch';
/* Volume slider */ /* Volume slider */
.slider { .slider {
min-width: 250px; min-width: 250px;
@ -13,9 +12,9 @@
--slider-height: 4px; --slider-height: 4px;
--slider-connect-bg: hsl(0, 0%, 21%); --slider-connect-bg: hsl(0, 0%, 21%);
--slider-tooltip-bg: hsl(0, 0%, 21%); --slider-tooltip-bg: hsl(0, 0%, 21%);
--slider-handle-ring-color: #3B82F630; --slider-handle-ring-color: #3b82f630;
--slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.32); --slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.32);
--slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.42); --slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.42);
} }
/* Now playing progress bar */ /* Now playing progress bar */
@ -33,8 +32,8 @@
--slider-handle-width: 10px; --slider-handle-width: 10px;
--slider-handle-height: 10px; --slider-handle-height: 10px;
--slider-handle-radius: 9999px; --slider-handle-radius: 9999px;
--slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.32); --slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.32);
--slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.42); --slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.42);
--slider-handle-ring-width: 3px; --slider-handle-ring-width: 3px;
} }
@ -55,7 +54,7 @@
a.navbar-item { a.navbar-item {
outline: 0; outline: 0;
line-height: 1.5; line-height: 1.5;
padding: .5rem 1rem; padding: 0.5rem 1rem;
} }
.fd-expanded { .fd-expanded {
@ -181,7 +180,8 @@ section.hero + section.fd-content {
/* Use object-fit to properly size the cover artwork: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit */ /* Use object-fit to properly size the cover artwork: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit */
object-fit: contain; object-fit: contain;
object-position: center bottom; object-position: center bottom;
filter: drop-shadow(0px 0px 1px rgba(0,0,0,.3)) drop-shadow(0px 0px 10px rgba(0,0,0,.3)); filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.3))
drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.3));
/* Allow flex item to grow/shrink to fill the whole container size */ /* Allow flex item to grow/shrink to fill the whole container size */
flex-grow: 1; flex-grow: 1;
@ -199,11 +199,11 @@ section.hero + section.fd-content {
overflow: hidden; overflow: hidden;
} }
.sortable-chosen .media-right { .sortable-chosen .media-right {
visibility: hidden; visibility: hidden;
} }
.sortable-ghost h1, .sortable-ghost h2 { .sortable-ghost h1,
.sortable-ghost h2 {
color: hsl(348, 100%, 61%) !important; color: hsl(348, 100%, 61%) !important;
} }
@ -214,15 +214,17 @@ section.hero + section.fd-content {
/* Transition effect */ /* Transition effect */
.fade-leave-active { .fade-leave-active {
transition: opacity .2s ease; transition: opacity 0.2s ease;
} }
.fade-enter-active { .fade-enter-active {
transition: opacity .5s ease; transition: opacity 0.5s ease;
} }
.fade-enter-from, .fade-leave-to { .fade-enter-from,
.fade-leave-to {
opacity: 0; opacity: 0;
} }
.fade-enter-to, .fade-leave-from { .fade-enter-to,
.fade-leave-from {
opacity: 1; opacity: 1;
} }
@ -252,7 +254,7 @@ section.hero + section.fd-content {
} }
.dropdown-item:hover { .dropdown-item:hover {
background-color: hsl(0, 0%, 96%) background-color: hsl(0, 0%, 96%);
} }
.navbar-item .fd-navbar-item-level2 { .navbar-item .fd-navbar-item-level2 {
@ -276,18 +278,17 @@ hr.fd-navbar-divider {
overflow: scroll; overflow: scroll;
} }
.buttons { .buttons {
@include mobile { @include mobile {
&.fd-is-centered-mobile { &.fd-is-centered-mobile {
justify-content: center; justify-content: center;
&:not(.has-addons) { &:not(.has-addons) {
.button:not(.is-fullwidth) { .button:not(.is-fullwidth) {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
}
} }
} }
}
} }
} }
@ -306,11 +307,11 @@ hr.fd-navbar-divider {
.fd-overlay-fullscreen { .fd-overlay-fullscreen {
@extend .is-overlay; @extend .is-overlay;
z-index:25; z-index: 25;
background-color: rgba(10, 10, 10, 0.2); background-color: rgba(10, 10, 10, 0.2);
position: fixed; position: fixed;
} }
.hero-body { .hero-body {
padding: 1.5rem !important; padding: 1.5rem !important;
} }

View File

@ -5,7 +5,9 @@
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths has-text-centered-mobile"> <div class="column is-four-fifths has-text-centered-mobile">
<p class="heading"><b>OwnTone</b> - version {{ config.version }}</p> <p class="heading"><b>OwnTone</b> - version {{ config.version }}</p>
<h1 class="title is-4">{{ config.library_name }}</h1> <h1 class="title is-4">
{{ config.library_name }}
</h1>
</div> </div>
</div> </div>
</div> </div>
@ -25,8 +27,14 @@
<!-- Right side --> <!-- Right side -->
<div class="level-right"> <div class="level-right">
<div v-if="library.updating"><a class="button is-small is-loading">Update</a></div> <div v-if="library.updating">
<div v-else><a @click="showUpdateDialog()" class="button is-small">Update</a></div> <a class="button is-small is-loading">Update</a>
</div>
<div v-else>
<a class="button is-small" @click="showUpdateDialog()"
>Update</a
>
</div>
</div> </div>
</nav> </nav>
@ -34,27 +42,50 @@
<tbody> <tbody>
<tr> <tr>
<th>Artists</th> <th>Artists</th>
<td class="has-text-right">{{ $filters.number(library.artists) }}</td> <td class="has-text-right">
{{ $filters.number(library.artists) }}
</td>
</tr> </tr>
<tr> <tr>
<th>Albums</th> <th>Albums</th>
<td class="has-text-right">{{ $filters.number(library.albums) }}</td> <td class="has-text-right">
{{ $filters.number(library.albums) }}
</td>
</tr> </tr>
<tr> <tr>
<th>Tracks</th> <th>Tracks</th>
<td class="has-text-right">{{ $filters.number(library.songs) }}</td> <td class="has-text-right">
{{ $filters.number(library.songs) }}
</td>
</tr> </tr>
<tr> <tr>
<th>Total playtime</th> <th>Total playtime</th>
<td class="has-text-right">{{ $filters.duration(library.db_playtime * 1000, 'y [years], d [days], h [hours], m [minutes]') }}</td> <td class="has-text-right">
{{
$filters.duration(
library.db_playtime * 1000,
'y [years], d [days], h [hours], m [minutes]'
)
}}
</td>
</tr> </tr>
<tr> <tr>
<th>Library updated</th> <th>Library updated</th>
<td class="has-text-right">{{ $filters.timeFromNow(library.updated_at) }} <span class="has-text-grey">({{ $filters.time(library.updated_at, 'lll') }})</span></td> <td class="has-text-right">
{{ $filters.timeFromNow(library.updated_at) }}
<span class="has-text-grey"
>({{ $filters.time(library.updated_at, 'lll') }})</span
>
</td>
</tr> </tr>
<tr> <tr>
<th>Uptime</th> <th>Uptime</th>
<td class="has-text-right">{{ $filters.timeFromNow(library.started_at, true) }} <span class="has-text-grey">({{ $filters.time(library.started_at, 'll') }})</span></td> <td class="has-text-right">
{{ $filters.timeFromNow(library.started_at, true) }}
<span class="has-text-grey"
>({{ $filters.time(library.started_at, 'll') }})</span
>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -68,8 +99,20 @@
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="content has-text-centered-mobile"> <div class="content has-text-centered-mobile">
<p class="is-size-7">Compiled with support for {{ config.buildoptions.join(', ') }}.</p> <p class="is-size-7">
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/owntone/owntone-server/network/dependencies">more</a>.</p> Compiled with support for {{ config.buildoptions.join(', ') }}.
</p>
<p class="is-size-7">
Web interface built with <a href="http://bulma.io">Bulma</a>,
<a href="https://materialdesignicons.com/"
>Material Design Icons</a
>, <a href="https://vuejs.org/">Vue.js</a>,
<a href="https://github.com/mzabriskie/axios">axios</a> and
<a
href="https://github.com/owntone/owntone-server/network/dependencies"
>more</a
>.
</p>
</div> </div>
</div> </div>
</div> </div>
@ -84,7 +127,7 @@ import * as types from '@/store/mutation_types'
export default { export default {
name: 'PageAbout', name: 'PageAbout',
data () { data() {
return { return {
show_update_dropdown: false, show_update_dropdown: false,
show_update_library: false show_update_library: false
@ -92,24 +135,23 @@ export default {
}, },
computed: { computed: {
config () { config() {
return this.$store.state.config return this.$store.state.config
}, },
library () { library() {
return this.$store.state.library return this.$store.state.library
} }
}, },
methods: { methods: {
onClickOutside (event) { onClickOutside(event) {
this.show_update_dropdown = false this.show_update_dropdown = false
}, },
showUpdateDialog () { showUpdateDialog() {
this.$store.commit(types.SHOW_UPDATE_DIALOG, true) this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,31 +1,48 @@
<template> <template>
<content-with-hero> <content-with-hero>
<template v-slot:heading-left> <template #heading-left>
<h1 class="title is-5">{{ album.name }}</h1> <h1 class="title is-5">
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2> {{ album.name }}
</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal">
<a class="has-text-link" @click="open_artist">{{ album.artist }}</a>
</h2>
<div class="buttons fd-is-centered-mobile fd-has-margin-top"> <div class="buttons fd-is-centered-mobile fd-has-margin-top">
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_album_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<p class="image is-square fd-has-shadow fd-has-action"> <p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
@click="show_album_details_modal = true" /> @click="show_album_details_modal = true"
/>
</p> </p>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p> <p class="heading is-7 has-text-centered-mobile fd-has-margin-top">
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks> {{ album.track_count }} tracks
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" /> </p>
<list-tracks :tracks="tracks" :uris="album.uri" />
<modal-dialog-album
:show="show_album_details_modal"
:album="album"
@close="show_album_details_modal = false"
/>
</template> </template>
</content-with-hero> </content-with-hero>
</template> </template>
@ -55,7 +72,20 @@ export default {
name: 'PageAlbum', name: 'PageAlbum',
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
album: {}, album: {},
tracks: [], tracks: [],
@ -73,22 +103,8 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,43 +1,60 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="albums_list.indexList"></index-button-list> <index-button-list :index="albums_list.indexList" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Filter</p> <p class="heading" style="margin-bottom: 24px">Filter</p>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input id="switchHideSingles" type="checkbox" name="switchHideSingles" class="switch" v-model="hide_singles"> <input
id="switchHideSingles"
v-model="hide_singles"
type="checkbox"
name="switchHideSingles"
class="switch"
/>
<label for="switchHideSingles">Hide singles</label> <label for="switchHideSingles">Hide singles</label>
</div> </div>
<p class="help">If active, hides singles and albums with tracks that only appear in playlists.</p> <p class="help">
If active, hides singles and albums with tracks that only appear
in playlists.
</p>
</div> </div>
<div class="field" v-if="spotify_enabled"> <div v-if="spotify_enabled" class="field">
<div class="control"> <div class="control">
<input id="switchHideSpotify" type="checkbox" name="switchHideSpotify" class="switch" v-model="hide_spotify"> <input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
name="switchHideSpotify"
class="switch"
/>
<label for="switchHideSpotify">Hide albums from Spotify</label> <label for="switchHideSpotify">Hide albums from Spotify</label>
</div> </div>
<p class="help">If active, hides albums that only appear in your Spotify library.</p> <p class="help">
If active, hides albums that only appear in your Spotify
library.
</p>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Sort by</p> <p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu> <dropdown-menu v-model="sort" :options="sort_options" />
</div> </div>
</div> </div>
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p> <p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right />
</template> <template #content>
<template v-slot:content> <list-albums :albums="albums_list" />
<list-albums :albums="albums_list"></list-albums>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -60,17 +77,46 @@ const dataObject = {
set: function (vm, response) { set: function (vm, response) {
vm.albums = response.data vm.albums = response.data
vm.index_list = [...new Set(vm.albums.items vm.index_list = [
.filter(album => !vm.$store.state.hide_singles || album.track_count > 2) ...new Set(
.map(album => album.name_sort.charAt(0).toUpperCase()))] 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',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListAlbums, DropdownMenu }, components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListAlbums,
DropdownMenu
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (this.albums.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
albums: { items: [] }, albums: { items: [] },
sort_options: ['Name', 'Recently added', 'Recently released'] sort_options: ['Name', 'Recently added', 'Recently released']
@ -78,7 +124,7 @@ export default {
}, },
computed: { computed: {
albums_list () { albums_list() {
return new Albums(this.albums.items, { return new Albums(this.albums.items, {
hideSingles: this.hide_singles, hideSingles: this.hide_singles,
hideSpotify: this.hide_spotify, hideSpotify: this.hide_spotify,
@ -87,33 +133,33 @@ export default {
}) })
}, },
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
}, },
hide_singles: { hide_singles: {
get () { get() {
return this.$store.state.hide_singles return this.$store.state.hide_singles
}, },
set (value) { set(value) {
this.$store.commit(types.HIDE_SINGLES, value) this.$store.commit(types.HIDE_SINGLES, value)
} }
}, },
hide_spotify: { hide_spotify: {
get () { get() {
return this.$store.state.hide_spotify return this.$store.state.hide_spotify
}, },
set (value) { set(value) {
this.$store.commit(types.HIDE_SPOTIFY, value) this.$store.commit(types.HIDE_SPOTIFY, value)
} }
}, },
sort: { sort: {
get () { get() {
return this.$store.state.albums_sort return this.$store.state.albums_sort
}, },
set (value) { set(value) {
this.$store.commit(types.ALBUMS_SORT, value) this.$store.commit(types.ALBUMS_SORT, value)
} }
} }
@ -123,26 +169,8 @@ export default {
scrollToTop: function () { scrollToTop: function () {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
if (this.albums.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,30 +1,47 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Sort by</p> <p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu> <dropdown-menu v-model="sort" :options="sort_options" />
</div>
</div> </div>
</template> </div>
<template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p>
</template> </template>
<template v-slot:heading-right> <template #heading-left>
<p class="title is-4">
{{ artist.name }}
</p>
</template>
<template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_artist_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #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">
<list-albums :albums="albums_list"></list-albums> {{ artist.album_count }} albums |
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> <a class="has-text-link" @click="open_tracks"
>{{ artist.track_count }} tracks</a
>
</p>
<list-albums :albums="albums_list" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@close="show_artist_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -54,9 +71,27 @@ const dataObject = {
export default { export default {
name: 'PageArtist', name: 'PageArtist',
components: { ContentWithHeading, ListAlbums, ModalDialogArtist, DropdownMenu }, components: {
ContentWithHeading,
ListAlbums,
ModalDialogArtist,
DropdownMenu
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
artist: {}, artist: {},
albums: { items: [] }, albums: { items: [] },
@ -67,7 +102,7 @@ export default {
}, },
computed: { computed: {
albums_list () { albums_list() {
return new Albums(this.albums.items, { return new Albums(this.albums.items, {
sort: this.sort, sort: this.sort,
group: false group: false
@ -75,10 +110,10 @@ export default {
}, },
sort: { sort: {
get () { get() {
return this.$store.state.artist_albums_sort return this.$store.state.artist_albums_sort
}, },
set (value) { set(value) {
this.$store.commit(types.ARTIST_ALBUMS_SORT, value) this.$store.commit(types.ARTIST_ALBUMS_SORT, value)
} }
} }
@ -86,28 +121,19 @@ export default {
methods: { methods: {
open_tracks: function () { open_tracks: function () {
this.$router.push({ path: '/music/artists/' + this.artist.id + '/tracks' }) this.$router.push({
path: '/music/artists/' + this.artist.id + '/tracks'
})
}, },
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
)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,26 +1,43 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">
{{ artist.name }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_artist_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #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">
<list-tracks :tracks="tracks.items" :uris="track_uris"></list-tracks> <a class="has-text-link" @click="open_artist"
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> >{{ artist.album_count }} albums</a
>
| {{ artist.track_count }} tracks
</p>
<list-tracks :tracks="tracks.items" :uris="track_uris" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@close="show_artist_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -49,9 +66,27 @@ const dataObject = {
export default { export default {
name: 'PageArtistTracks', name: 'PageArtistTracks',
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist }, components: {
ContentWithHeading,
ListTracks,
IndexButtonList,
ModalDialogArtist
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
artist: {}, artist: {},
tracks: { items: [] }, tracks: { items: [] },
@ -61,13 +96,18 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.tracks.items return [
.map(track => track.title_sort.charAt(0).toUpperCase()))] ...new Set(
this.tracks.items.map((track) =>
track.title_sort.charAt(0).toUpperCase()
)
)
]
}, },
track_uris () { track_uris() {
return this.tracks.items.map(a => a.uri).join(',') return this.tracks.items.map((a) => a.uri).join(',')
} }
}, },
@ -78,24 +118,13 @@ 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
)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,43 +1,62 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="artists_list.indexList"></index-button-list> <index-button-list :index="artists_list.indexList" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Filter</p> <p class="heading" style="margin-bottom: 24px">Filter</p>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input id="switchHideSingles" type="checkbox" name="switchHideSingles" class="switch" v-model="hide_singles"> <input
id="switchHideSingles"
v-model="hide_singles"
type="checkbox"
name="switchHideSingles"
class="switch"
/>
<label for="switchHideSingles">Hide singles</label> <label for="switchHideSingles">Hide singles</label>
</div> </div>
<p class="help">If active, hides artists that only appear on singles or playlists.</p> <p class="help">
If active, hides artists that only appear on singles or
playlists.
</p>
</div> </div>
<div class="field" v-if="spotify_enabled"> <div v-if="spotify_enabled" class="field">
<div class="control"> <div class="control">
<input id="switchHideSpotify" type="checkbox" name="switchHideSpotify" class="switch" v-model="hide_spotify"> <input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
name="switchHideSpotify"
class="switch"
/>
<label for="switchHideSpotify">Hide artists from Spotify</label> <label for="switchHideSpotify">Hide artists from Spotify</label>
</div> </div>
<p class="help">If active, hides artists that only appear in your Spotify library.</p> <p class="help">
If active, hides artists that only appear in your Spotify
library.
</p>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Sort by</p> <p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu> <dropdown-menu v-model="sort" :options="sort_options" />
</div> </div>
</div> </div>
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Artists</p> <p class="heading">
{{ artists_list.sortedAndFiltered.length }} Artists
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right />
</template> <template #content>
<template v-slot:content> <list-artists :artists="artists_list" />
<list-artists :artists="artists_list"></list-artists>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -65,9 +84,32 @@ const dataObject = {
export default { export default {
name: 'PageArtists', name: 'PageArtists',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu }, components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListArtists,
DropdownMenu
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (this.artists.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
artists: { items: [] }, artists: { items: [] },
sort_options: ['Name', 'Recently added'] sort_options: ['Name', 'Recently added']
@ -75,7 +117,7 @@ export default {
}, },
computed: { computed: {
artists_list () { artists_list() {
return new Artists(this.artists.items, { return new Artists(this.artists.items, {
hideSingles: this.hide_singles, hideSingles: this.hide_singles,
hideSpotify: this.hide_spotify, hideSpotify: this.hide_spotify,
@ -84,33 +126,33 @@ export default {
}) })
}, },
spotify_enabled () { spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid return this.$store.state.spotify.webapi_token_valid
}, },
hide_singles: { hide_singles: {
get () { get() {
return this.$store.state.hide_singles return this.$store.state.hide_singles
}, },
set (value) { set(value) {
this.$store.commit(types.HIDE_SINGLES, value) this.$store.commit(types.HIDE_SINGLES, value)
} }
}, },
hide_spotify: { hide_spotify: {
get () { get() {
return this.$store.state.hide_spotify return this.$store.state.hide_spotify
}, },
set (value) { set(value) {
this.$store.commit(types.HIDE_SPOTIFY, value) this.$store.commit(types.HIDE_SPOTIFY, value)
} }
}, },
sort: { sort: {
get () { get() {
return this.$store.state.artists_sort return this.$store.state.artists_sort
}, },
set (value) { set(value) {
this.$store.commit(types.ARTISTS_SORT, value) this.$store.commit(types.ARTISTS_SORT, value)
} }
} }
@ -120,26 +162,8 @@ export default {
scrollToTop: function () { scrollToTop: function () {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
if (this.artists.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,31 +1,49 @@
<template> <template>
<content-with-hero> <content-with-hero>
<template v-slot:heading-left> <template #heading-left>
<h1 class="title is-5">{{ album.name }}</h1> <h1 class="title is-5">
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2> {{ album.name }}
</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal">
<a class="has-text-link" @click="open_artist">{{ album.artist }}</a>
</h2>
<div class="buttons fd-is-centered-mobile fd-has-margin-top"> <div class="buttons fd-is-centered-mobile fd-has-margin-top">
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Play</span> <span class="icon"><i class="mdi mdi-play" /></span>
<span>Play</span>
</a> </a>
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_album_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<p class="image is-square fd-has-shadow fd-has-action"> <p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
@click="show_album_details_modal = true" /> @click="show_album_details_modal = true"
/>
</p> </p>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p> <p class="heading is-7 has-text-centered-mobile fd-has-margin-top">
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks> {{ album.track_count }} tracks
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" /> </p>
<list-tracks :tracks="tracks" :uris="album.uri" />
<modal-dialog-album
:show="show_album_details_modal"
:album="album"
:media_kind="'audiobook'"
@close="show_album_details_modal = false"
/>
</template> </template>
</content-with-hero> </content-with-hero>
</template> </template>
@ -55,7 +73,20 @@ export default {
name: 'PageAudiobooksAlbum', name: 'PageAudiobooksAlbum',
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
album: {}, album: {},
tracks: [], tracks: [],
@ -82,22 +113,8 @@ export default {
this.selected_track = track this.selected_track = track
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,17 +1,19 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-audiobooks></tabs-audiobooks> <tabs-audiobooks />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="albums_list.indexList"></index-button-list> <index-button-list :index="albums_list.indexList" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Audiobooks</p> <p class="title is-4">Audiobooks</p>
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Audiobooks</p> <p class="heading">
{{ albums_list.sortedAndFiltered.length }} Audiobooks
</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -37,16 +39,34 @@ const dataObject = {
export default { export default {
name: 'PageAudiobooksAlbums', name: 'PageAudiobooksAlbums',
components: { TabsAudiobooks, ContentWithHeading, IndexButtonList, ListAlbums }, components: {
TabsAudiobooks,
ContentWithHeading,
IndexButtonList,
ListAlbums
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
albums: { items: [] } albums: { items: [] }
} }
}, },
computed: { computed: {
albums_list () { albums_list() {
return new Albums(this.albums.items, { return new Albums(this.albums.items, {
sort: 'Name', sort: 'Name',
group: true group: true
@ -54,23 +74,8 @@ export default {
} }
}, },
methods: { methods: {}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
}
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,22 +1,36 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">
{{ artist.name }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_artist_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-play" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums</p> <p class="heading has-text-centered-mobile">
<list-albums :albums="albums.items"></list-albums> {{ artist.album_count }} albums
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> </p>
<list-albums :albums="albums.items" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@close="show_artist_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -45,7 +59,20 @@ export default {
name: 'PageAudiobooksArtist', name: 'PageAudiobooksArtist',
components: { ContentWithHeading, ListAlbums, ModalDialogArtist }, components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
artist: {}, artist: {},
albums: {}, albums: {},
@ -56,24 +83,13 @@ export default {
methods: { methods: {
play: function () { play: function () {
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), false) webapi.player_play_uri(
this.albums.items.map((a) => a.uri).join(','),
false
)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,19 +1,20 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-audiobooks></tabs-audiobooks> <tabs-audiobooks />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="artists_list.indexList"></index-button-list> <index-button-list :index="artists_list.indexList" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Authors</p> <p class="title is-4">Authors</p>
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Authors</p> <p class="heading">
{{ artists_list.sortedAndFiltered.length }} Authors
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right />
</template> <template #content>
<template v-slot:content> <list-artists :artists="artists_list" />
<list-artists :artists="artists_list"></list-artists>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -39,16 +40,34 @@ const dataObject = {
export default { export default {
name: 'PageAudiobooksArtists', name: 'PageAudiobooksArtists',
components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists }, components: {
ContentWithHeading,
TabsAudiobooks,
IndexButtonList,
ListArtists
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
artists: { items: [] } artists: { items: [] }
} }
}, },
computed: { computed: {
artists_list () { artists_list() {
return new Artists(this.artists.items, { return new Artists(this.artists.items, {
sort: 'Name', sort: 'Name',
group: true group: true
@ -56,23 +75,8 @@ export default {
} }
}, },
methods: { methods: {}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
}
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,20 +1,24 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<!-- Recently added --> <!-- Recently added -->
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Recently added</p> <p class="title is-4">Recently added</p>
<p class="heading">albums</p> <p class="heading">albums</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="recently_added.items"></list-albums> <list-albums :albums="recently_added.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_browse('recently_added')">Show more</a> <a
class="button is-light is-small is-rounded"
@click="open_browse('recently_added')"
>Show more</a
>
</p> </p>
</nav> </nav>
</template> </template>
@ -22,17 +26,21 @@
<!-- Recently played --> <!-- Recently played -->
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Recently played</p> <p class="title is-4">Recently played</p>
<p class="heading">tracks</p> <p class="heading">tracks</p>
</template> </template>
<template v-slot:content> <template #content>
<list-tracks :tracks="recently_played.items"></list-tracks> <list-tracks :tracks="recently_played.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_browse('recently_played')">Show more</a> <a
class="button is-light is-small is-rounded"
@click="open_browse('recently_played')"
>Show more</a
>
</p> </p>
</nav> </nav>
</template> </template>
@ -50,8 +58,18 @@ import webapi from '@/webapi'
const dataObject = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.search({ type: 'album', expression: 'time_added after 8 weeks ago and media_kind is music having track_count > 3 order by time_added desc', limit: 3 }), webapi.search({
webapi.search({ type: 'track', expression: 'time_played after 8 weeks ago and media_kind is music order by time_played desc', limit: 3 }) type: 'album',
expression:
'time_added after 8 weeks ago and media_kind is music having track_count > 3 order by time_added desc',
limit: 3
}),
webapi.search({
type: 'track',
expression:
'time_played after 8 weeks ago and media_kind is music order by time_played desc',
limit: 3
})
]) ])
}, },
@ -65,7 +83,20 @@ export default {
name: 'PageBrowse', name: 'PageBrowse',
components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks }, components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
recently_added: { items: [] }, recently_added: { items: [] },
recently_played: { items: [] }, recently_played: { items: [] },
@ -79,22 +110,8 @@ export default {
open_browse: function (type) { open_browse: function (type) {
this.$router.push({ path: '/music/browse/' + type }) this.$router.push({ path: '/music/browse/' + type })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Recently added</p> <p class="title is-4">Recently added</p>
<p class="heading">albums</p> <p class="heading">albums</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -27,7 +27,8 @@ const dataObject = {
const limit = store.getters.settings_option_recently_added_limit const limit = store.getters.settings_option_recently_added_limit
return webapi.search({ return webapi.search({
type: 'album', type: 'album',
expression: 'media_kind is music having track_count > 3 order by time_added desc', expression:
'media_kind is music having track_count > 3 order by time_added desc',
limit: limit limit: limit
}) })
}, },
@ -41,14 +42,27 @@ export default {
name: 'PageBrowseType', name: 'PageBrowseType',
components: { ContentWithHeading, TabsMusic, ListAlbums }, components: { ContentWithHeading, TabsMusic, ListAlbums },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
recently_added: { items: [] } recently_added: { items: [] }
} }
}, },
computed: { computed: {
albums_list () { albums_list() {
return new Albums(this.recently_added.items, { return new Albums(this.recently_added.items, {
hideSingles: false, hideSingles: false,
hideSpotify: false, hideSpotify: false,
@ -56,22 +70,8 @@ export default {
group: true group: true
}) })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Recently played</p> <p class="title is-4">Recently played</p>
<p class="heading">tracks</p> <p class="heading">tracks</p>
</template> </template>
<template v-slot:content> <template #content>
<list-tracks :tracks="recently_played.items"></list-tracks> <list-tracks :tracks="recently_played.items" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -24,7 +24,8 @@ const dataObject = {
load: function (to) { load: function (to) {
return webapi.search({ return webapi.search({
type: 'track', type: 'track',
expression: 'time_played after 8 weeks ago and media_kind is music order by time_played desc', expression:
'time_played after 8 weeks ago and media_kind is music order by time_played desc',
limit: 50 limit: 50
}) })
}, },
@ -38,26 +39,25 @@ export default {
name: 'PageBrowseType', name: 'PageBrowseType',
components: { ContentWithHeading, TabsMusic, ListTracks }, components: { ContentWithHeading, TabsMusic, ListTracks },
data () { beforeRouteEnter(to, from, next) {
return {
recently_played: {}
}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },
beforeRouteUpdate (to, from, next) { beforeRouteUpdate(to, from, next) {
const vm = this const vm = this
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
dataObject.set(vm, response) dataObject.set(vm, response)
next() next()
}) })
},
data() {
return {
recently_played: {}
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,33 +1,59 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ name }}</p> <p class="title is-4">
{{ name }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_composer_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ composer_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p> <p class="heading has-text-centered-mobile">
<list-item-albums v-for="album in composer_albums.items" :key="album.id" :album="album" @click="open_album(album)"> {{ composer_albums.total }} albums |
<a class="has-text-link" @click="open_tracks">tracks</a>
</p>
<list-item-albums
v-for="album in composer_albums.items"
:key="album.id"
:album="album"
@click="open_album(album)"
>
<template slot:actions> <template slot:actions>
<a @click="open_dialog(album)"> <a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-albums> </list-item-albums>
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" /> <modal-dialog-album
<modal-dialog-composer :show="show_composer_details_modal" :composer="{ 'name': name }" @close="show_composer_details_modal = false" /> :show="show_details_modal"
:album="selected_album"
@close="show_details_modal = false"
/>
<modal-dialog-composer
:show="show_composer_details_modal"
:composer="{ name: name }"
@close="show_composer_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -53,9 +79,27 @@ const dataObject = {
export default { export default {
name: 'PageComposer', name: 'PageComposer',
components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer }, components: {
ContentWithHeading,
ListItemAlbums,
ModalDialogAlbum,
ModalDialogComposer
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
name: '', name: '',
composer_albums: { items: [] }, composer_albums: { items: [] },
@ -67,20 +111,31 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.composer_albums.items return [
.map(album => album.name_sort.charAt(0).toUpperCase()))] ...new Set(
this.composer_albums.items.map((album) =>
album.name_sort.charAt(0).toUpperCase()
)
)
]
} }
}, },
methods: { methods: {
open_tracks: function () { open_tracks: function () {
this.show_details_modal = false this.show_details_modal = false
this.$router.push({ name: 'ComposerTracks', params: { composer: this.name } }) this.$router.push({
name: 'ComposerTracks',
params: { composer: this.name }
})
}, },
play: function () { play: function () {
webapi.player_play_expression('composer is "' + this.name + '" and media_kind is music', true) webapi.player_play_expression(
'composer is "' + this.name + '" and media_kind is music',
true
)
}, },
open_album: function (album) { open_album: function (album) {
@ -91,22 +146,8 @@ export default {
this.selected_album = album this.selected_album = album
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,33 +1,59 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ composer }}</p> <p class="title is-4">
{{ composer }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_composer_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_albums">albums</a> | {{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">
<list-item-track v-for="(track, index) in rated_tracks" :key="track.id" :track="track" @click="play_track(index)"> <a class="has-text-link" @click="open_albums">albums</a> |
<template v-slot:actions> {{ tracks.total }} tracks
</p>
<list-item-track
v-for="(track, index) in rated_tracks"
:key="track.id"
:track="track"
@click="play_track(index)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-track> </list-item-track>
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" /> <modal-dialog-track
<modal-dialog-composer :show="show_composer_details_modal" :composer="{ 'name': composer }" @close="show_composer_details_modal = false" /> :show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
/>
<modal-dialog-composer
:show="show_composer_details_modal"
:composer="{ name: composer }"
@close="show_composer_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -53,9 +79,27 @@ const dataObject = {
export default { export default {
name: 'PageComposerTracks', name: 'PageComposerTracks',
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogComposer }, components: {
ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ModalDialogComposer
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
tracks: { items: [] }, tracks: { items: [] },
composer: '', composer: '',
@ -70,28 +114,45 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.tracks.items return [
.map(track => track.title_sort.charAt(0).toUpperCase()))] ...new Set(
this.tracks.items.map((track) =>
track.title_sort.charAt(0).toUpperCase()
)
)
]
}, },
rated_tracks () { rated_tracks() {
return this.tracks.items.filter(track => track.rating >= this.min_rating) return this.tracks.items.filter(
(track) => track.rating >= this.min_rating
)
} }
}, },
methods: { methods: {
open_albums: function () { open_albums: function () {
this.show_details_modal = false this.show_details_modal = false
this.$router.push({ name: 'ComposerAlbums', params: { composer: this.composer } }) this.$router.push({
name: 'ComposerAlbums',
params: { composer: this.composer }
})
}, },
play: function () { play: function () {
webapi.player_play_expression('composer is "' + this.composer + '" and media_kind is music', true) webapi.player_play_expression(
'composer is "' + this.composer + '" and media_kind is music',
true
)
}, },
play_track: function (position) { play_track: function (position) {
webapi.player_play_expression('composer is "' + this.composer + '" and media_kind is music', false, position) webapi.player_play_expression(
'composer is "' + this.composer + '" and media_kind is music',
false,
position
)
}, },
show_rating: function (rating) { show_rating: function (rating) {
@ -105,22 +166,8 @@ export default {
this.selected_track = track this.selected_track = track
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,17 +1,19 @@
<template> <template>
<div> <div>
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="composers_list.indexList"></index-button-list> <index-button-list :index="composers_list.indexList" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ heading }}</p> <p class="title is-4">
{{ heading }}
</p>
<p class="heading">{{ composers.total }} composers</p> <p class="heading">{{ composers.total }} composers</p>
</template> </template>
<template v-slot:content> <template #content>
<list-composers :composers="composers_list"></list-composers> <list-composers :composers="composers_list" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -45,7 +47,20 @@ export default {
name: 'PageComposers', name: 'PageComposers',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
composers: { items: [] }, composers: { items: [] },
heading: '', heading: '',
@ -56,12 +71,17 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.composers.items return [
.map(composer => composer.name.charAt(0).toUpperCase()))] ...new Set(
this.composers.items.map((composer) =>
composer.name.charAt(0).toUpperCase()
)
)
]
}, },
composers_list () { composers_list() {
return new Composers(this.composers.items, { return new Composers(this.composers.items, {
sort: 'Name', sort: 'Name',
group: true group: true
@ -71,29 +91,18 @@ export default {
methods: { methods: {
open_composer: function (composer) { open_composer: function (composer) {
this.$router.push({ name: 'ComposerAlbums', params: { composer: composer.name } }) this.$router.push({
name: 'ComposerAlbums',
params: { composer: composer.name }
})
}, },
open_dialog: function (composer) { open_dialog: function (composer) {
this.selected_composer = composer this.selected_composer = composer
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,72 +1,117 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Files</p> <p class="title is-4">Files</p>
<p class="title is-7 has-text-grey">{{ current_directory }}</p> <p class="title is-7 has-text-grey">
{{ current_directory }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="open_directory_dialog({ 'path': current_directory })"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="open_directory_dialog({ path: current_directory })"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Play</span> <span class="icon"><i class="mdi mdi-play" /></span>
<span>Play</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<div class="media" v-if="$route.query.directory" @click="open_parent_directory()"> <div
v-if="$route.query.directory"
class="media"
@click="open_parent_directory()"
>
<figure class="media-left fd-has-action"> <figure class="media-left fd-has-action">
<span class="icon"> <span class="icon">
<i class="mdi mdi-subdirectory-arrow-left"></i> <i class="mdi mdi-subdirectory-arrow-left" />
</span> </span>
</figure> </figure>
<div class="media-content fd-has-action is-clipped"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">..</h1> <h1 class="title is-6">..</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions" />
</div> </div>
</div> </div>
<list-item-directory v-for="directory in files.directories" :key="directory.path" :directory="directory" @click="open_directory(directory)"> <list-item-directory
<template v-slot:actions> v-for="directory in files.directories"
<a @click="open_directory_dialog(directory)"> :key="directory.path"
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> :directory="directory"
</a> @click="open_directory(directory)"
</template> >
<template #actions>
<a @click="open_directory_dialog(directory)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-directory> </list-item-directory>
<list-item-playlist v-for="playlist in files.playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)"> <list-item-playlist
<template v-slot:icon> v-for="playlist in files.playlists.items"
:key="playlist.id"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<template #icon>
<span class="icon"> <span class="icon">
<i class="mdi mdi-library-music"></i> <i class="mdi mdi-library-music" />
</span> </span>
</template> </template>
<template v-slot:actions> <template #actions>
<a @click="open_playlist_dialog(playlist)"> <a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-playlist> </list-item-playlist>
<list-item-track v-for="(track, index) in files.tracks.items" :key="track.id" :track="track" @click="play_track(index)"> <list-item-track
<template v-slot:icon> v-for="(track, index) in files.tracks.items"
:key="track.id"
:track="track"
@click="play_track(index)"
>
<template #icon>
<span class="icon"> <span class="icon">
<i class="mdi mdi-file-outline"></i> <i class="mdi mdi-file-outline" />
</span> </span>
</template> </template>
<template v-slot:actions> <template #actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-track> </list-item-track>
<modal-dialog-directory :show="show_directory_details_modal" :directory="selected_directory" @close="show_directory_details_modal = false" /> <modal-dialog-directory
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" /> :show="show_directory_details_modal"
<modal-dialog-track :show="show_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" /> :directory="selected_directory"
@close="show_directory_details_modal = false"
/>
<modal-dialog-playlist
:show="show_playlist_details_modal"
:playlist="selected_playlist"
@close="show_playlist_details_modal = false"
/>
<modal-dialog-track
:show="show_track_details_modal"
:track="selected_track"
@close="show_track_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -95,7 +140,9 @@ const dataObject = {
vm.files = response.data vm.files = response.data
} else { } else {
vm.files = { vm.files = {
directories: vm.$store.state.config.directories.map(dir => { return { path: dir } }), directories: vm.$store.state.config.directories.map((dir) => {
return { path: dir }
}),
tracks: { items: [] }, tracks: { items: [] },
playlists: { items: [] } playlists: { items: [] }
} }
@ -105,11 +152,36 @@ const dataObject = {
export default { export default {
name: 'PageFiles', name: 'PageFiles',
components: { ContentWithHeading, ListItemDirectory, ListItemPlaylist, ListItemTrack, ModalDialogDirectory, ModalDialogPlaylist, ModalDialogTrack }, components: {
ContentWithHeading,
ListItemDirectory,
ListItemPlaylist,
ListItemTrack,
ModalDialogDirectory,
ModalDialogPlaylist,
ModalDialogTrack
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
files: { directories: [], tracks: { items: [] }, playlists: { items: [] } }, files: {
directories: [],
tracks: { items: [] },
playlists: { items: [] }
},
show_directory_details_modal: false, show_directory_details_modal: false,
selected_directory: {}, selected_directory: {},
@ -123,7 +195,7 @@ export default {
}, },
computed: { computed: {
current_directory () { current_directory() {
if (this.$route.query && this.$route.query.directory) { if (this.$route.query && this.$route.query.directory) {
return this.$route.query.directory return this.$route.query.directory
} }
@ -133,16 +205,33 @@ export default {
methods: { methods: {
open_parent_directory: function () { open_parent_directory: function () {
const parent = this.current_directory.slice(0, this.current_directory.lastIndexOf('/')) const parent = this.current_directory.slice(
if (parent === '' || this.$store.state.config.directories.includes(this.current_directory)) { 0,
this.current_directory.lastIndexOf('/')
)
if (
parent === '' ||
this.$store.state.config.directories.includes(this.current_directory)
) {
this.$router.push({ path: '/files' }) this.$router.push({ path: '/files' })
} else { } else {
this.$router.push({ path: '/files', query: { directory: this.current_directory.slice(0, this.current_directory.lastIndexOf('/')) } }) this.$router.push({
path: '/files',
query: {
directory: this.current_directory.slice(
0,
this.current_directory.lastIndexOf('/')
)
}
})
} }
}, },
open_directory: function (directory) { open_directory: function (directory) {
this.$router.push({ path: '/files', query: { directory: directory.path } }) this.$router.push({
path: '/files',
query: { directory: directory.path }
})
}, },
open_directory_dialog: function (directory) { open_directory_dialog: function (directory) {
@ -151,11 +240,18 @@ export default {
}, },
play: function () { play: function () {
webapi.player_play_expression('path starts with "' + this.current_directory + '" order by path asc', false) webapi.player_play_expression(
'path starts with "' + this.current_directory + '" order by path asc',
false
)
}, },
play_track: function (position) { play_track: function (position) {
webapi.player_play_uri(this.files.tracks.items.map(a => a.uri).join(','), false, position) webapi.player_play_uri(
this.files.tracks.items.map((a) => a.uri).join(','),
false,
position
)
}, },
open_track_dialog: function (track) { open_track_dialog: function (track) {
@ -171,22 +267,8 @@ export default {
this.selected_playlist = playlist this.selected_playlist = playlist
this.show_playlist_details_modal = true this.show_playlist_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,26 +1,41 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ name }}</p> <p class="title is-4">
{{ name }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_genre_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p> <p class="heading has-text-centered-mobile">
<list-albums :albums="genre_albums.items"></list-albums> {{ genre_albums.total }} albums |
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" /> <a class="has-text-link" @click="open_tracks">tracks</a>
</p>
<list-albums :albums="genre_albums.items" />
<modal-dialog-genre
:show="show_genre_details_modal"
:genre="{ name: name }"
@close="show_genre_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -46,9 +61,27 @@ const dataObject = {
export default { export default {
name: 'PageGenre', name: 'PageGenre',
components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre }, components: {
ContentWithHeading,
IndexButtonList,
ListAlbums,
ModalDialogGenre
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
name: '', name: '',
genre_albums: { items: [] }, genre_albums: { items: [] },
@ -58,9 +91,14 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.genre_albums.items return [
.map(album => album.name.charAt(0).toUpperCase()))] ...new Set(
this.genre_albums.items.map((album) =>
album.name.charAt(0).toUpperCase()
)
)
]
} }
}, },
@ -71,29 +109,18 @@ export default {
}, },
play: function () { play: function () {
webapi.player_play_expression('genre is "' + this.name + '" and media_kind is music', true) webapi.player_play_expression(
'genre is "' + this.name + '" and media_kind is music',
true
)
}, },
open_dialog: function (album) { open_dialog: function (album) {
this.selected_album = album this.selected_album = album
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,26 +1,41 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ genre }}</p> <p class="title is-4">
{{ genre }}
</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_genre_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">
<list-tracks :tracks="tracks.items" :expression="expression"></list-tracks> <a class="has-text-link" @click="open_genre">albums</a> |
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" /> {{ tracks.total }} tracks
</p>
<list-tracks :tracks="tracks.items" :expression="expression" />
<modal-dialog-genre
:show="show_genre_details_modal"
:genre="{ name: genre }"
@close="show_genre_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -46,9 +61,27 @@ const dataObject = {
export default { export default {
name: 'PageGenreTracks', name: 'PageGenreTracks',
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre }, components: {
ContentWithHeading,
ListTracks,
IndexButtonList,
ModalDialogGenre
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
tracks: { items: [] }, tracks: { items: [] },
genre: '', genre: '',
@ -58,12 +91,17 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.tracks.items return [
.map(track => track.title_sort.charAt(0).toUpperCase()))] ...new Set(
this.tracks.items.map((track) =>
track.title_sort.charAt(0).toUpperCase()
)
)
]
}, },
expression () { expression() {
return 'genre is "' + this.genre + '" and media_kind is music' return 'genre is "' + this.genre + '" and media_kind is music'
} }
}, },
@ -77,22 +115,8 @@ export default {
play: function () { play: function () {
webapi.player_play_expression(this.expression, true) webapi.player_play_expression(this.expression, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,24 +1,35 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music />
<content-with-heading> <content-with-heading>
<template v-slot:options> <template #options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list" />
</template> </template>
<template v-slot:heading-left> <template #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 v-slot:content> <template #content>
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre" @click="open_genre(genre)"> <list-item-genre
<template v-slot:actions> v-for="genre in genres.items"
:key="genre.name"
:genre="genre"
@click="open_genre(genre)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(genre)"> <a @click.prevent.stop="open_dialog(genre)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-genre> </list-item-genre>
<modal-dialog-genre :show="show_details_modal" :genre="selected_genre" @close="show_details_modal = false" /> <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>
@ -44,9 +55,28 @@ const dataObject = {
export default { export default {
name: 'PageGenres', name: 'PageGenres',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre }, components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListItemGenre,
ModalDialogGenre
},
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
genres: { items: [] }, genres: { items: [] },
@ -56,9 +86,12 @@ export default {
}, },
computed: { computed: {
index_list () { index_list() {
return [...new Set(this.genres.items return [
.map(genre => genre.name.charAt(0).toUpperCase()))] ...new Set(
this.genres.items.map((genre) => genre.name.charAt(0).toUpperCase())
)
]
} }
}, },
@ -71,22 +104,8 @@ export default {
this.selected_genre = genre this.selected_genre = genre
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -2,23 +2,27 @@
<section> <section>
<div v-if="now_playing.id > 0" class="fd-is-fullheight"> <div v-if="now_playing.id > 0" class="fd-is-fullheight">
<div class="fd-is-expanded"> <div class="fd-is-expanded">
<cover-artwork @click="open_dialog(now_playing)" <cover-artwork
:artwork_url="now_playing.artwork_url" :artwork_url="now_playing.artwork_url"
:artist="now_playing.artist" :artist="now_playing.artist"
:album="now_playing.album" :album="now_playing.album"
class="fd-cover-image fd-has-action" /> class="fd-cover-image fd-has-action"
@click="open_dialog(now_playing)"
/>
</div> </div>
<div class="fd-has-padding-left-right"> <div class="fd-has-padding-left-right">
<div class="container has-text-centered"> <div class="container has-text-centered">
<p class="control has-text-centered fd-progress-now-playing"> <p class="control has-text-centered fd-progress-now-playing">
<Slider v-model="item_progress_ms" <Slider
v-model="item_progress_ms"
:min="0" :min="0"
:max="state.item_length_ms" :max="state.item_length_ms"
:step="1000" :step="1000"
:tooltips="false" :tooltips="false"
:disabled="state.state === 'stop'" :disabled="state.state === 'stop'"
:classes="{ target: 'seek-slider' }"
@change="seek" @change="seek"
:classes="{ target: 'seek-slider'}" /> />
<!--range-slider <!--range-slider
class="seek-slider fd-has-action" class="seek-slider fd-has-action"
min="0" min="0"
@ -30,7 +34,10 @@
</range-slider--> </range-slider-->
</p> </p>
<p class="content"> <p class="content">
<span>{{ $filters.duration(item_progress_ms) }} / {{ $filters.duration(now_playing.length_ms) }}</span> <span
>{{ $filters.duration(item_progress_ms) }} /
{{ $filters.duration(now_playing.length_ms) }}</span
>
</p> </p>
</div> </div>
</div> </div>
@ -42,8 +49,11 @@
<h2 class="title is-6"> <h2 class="title is-6">
{{ now_playing.artist }} {{ now_playing.artist }}
</h2> </h2>
<h2 class="subtitle is-6 has-text-grey has-text-weight-bold" v-if="composer"> <h2
{{ composer }} v-if="composer"
class="subtitle is-6 has-text-grey has-text-weight-bold"
>
{{ composer }}
</h2> </h2>
<h3 class="subtitle is-6"> <h3 class="subtitle is-6">
{{ now_playing.album }} {{ now_playing.album }}
@ -52,18 +62,21 @@
</div> </div>
</div> </div>
<div v-else class="fd-is-fullheight"> <div v-else class="fd-is-fullheight">
<div class="fd-is-expanded fd-has-padding-left-right" style="flex-direction: column;"> <div
class="fd-is-expanded fd-has-padding-left-right"
style="flex-direction: column"
>
<div class="content has-text-centered"> <div class="content has-text-centered">
<h1 class="title is-5"> <h1 class="title is-5">Your play queue is empty</h1>
Your play queue is empty <p>Add some tracks by browsing your library</p>
</h1>
<p>
Add some tracks by browsing your library
</p>
</div> </div>
</div> </div>
</div> </div>
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" /> <modal-dialog-queue-item
:show="show_details_modal"
:item="selected_item"
@close="show_details_modal = false"
/>
</section> </section>
</template> </template>
@ -79,12 +92,12 @@ export default {
name: 'PageNowPlaying', name: 'PageNowPlaying',
components: { components: {
ModalDialogQueueItem, ModalDialogQueueItem,
// RangeSlider, // RangeSlider,
Slider, Slider,
CoverArtwork CoverArtwork
}, },
data () { data() {
return { return {
item_progress_ms: 0, item_progress_ms: 0,
interval_id: 0, interval_id: 0,
@ -94,7 +107,57 @@ export default {
} }
}, },
created () { computed: {
state() {
return this.$store.state.player
},
now_playing() {
return this.$store.getters.now_playing
},
settings_option_show_composer_now_playing() {
return this.$store.getters.settings_option_show_composer_now_playing
},
settings_option_show_composer_for_genre() {
return this.$store.getters.settings_option_show_composer_for_genre
},
composer() {
if (this.settings_option_show_composer_now_playing) {
if (
!this.settings_option_show_composer_for_genre ||
(this.now_playing.genre &&
this.settings_option_show_composer_for_genre
.toLowerCase()
.split(',')
.findIndex(
(elem) =>
this.now_playing.genre.toLowerCase().indexOf(elem.trim()) >= 0
) >= 0)
) {
return this.now_playing.composer
}
}
return null
}
},
watch: {
state() {
if (this.interval_id > 0) {
window.clearTimeout(this.interval_id)
this.interval_id = 0
}
this.item_progress_ms = this.state.item_progress_ms
if (this.state.state === 'play') {
this.interval_id = window.setInterval(this.tick, 1000)
}
}
},
created() {
this.item_progress_ms = this.state.item_progress_ms this.item_progress_ms = this.state.item_progress_ms
webapi.player_status().then(({ data }) => { webapi.player_status().then(({ data }) => {
this.$store.commit(types.UPDATE_PLAYER_STATUS, data) this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
@ -104,44 +167,13 @@ export default {
}) })
}, },
destroyed () { unmounted() {
if (this.interval_id > 0) { if (this.interval_id > 0) {
window.clearTimeout(this.interval_id) window.clearTimeout(this.interval_id)
this.interval_id = 0 this.interval_id = 0
} }
}, },
computed: {
state () {
return this.$store.state.player
},
now_playing () {
return this.$store.getters.now_playing
},
settings_option_show_composer_now_playing () {
return this.$store.getters.settings_option_show_composer_now_playing
},
settings_option_show_composer_for_genre () {
return this.$store.getters.settings_option_show_composer_for_genre
},
composer () {
if (this.settings_option_show_composer_now_playing) {
if (!this.settings_option_show_composer_for_genre ||
(this.now_playing.genre &&
this.settings_option_show_composer_for_genre.toLowerCase()
.split(',')
.findIndex(elem => this.now_playing.genre.toLowerCase().indexOf(elem.trim()) >= 0) >= 0)) {
return this.now_playing.composer
}
}
return null
}
},
methods: { methods: {
tick: function () { tick: function () {
this.item_progress_ms += 1000 this.item_progress_ms += 1000
@ -157,22 +189,8 @@ export default {
this.selected_item = item this.selected_item = item
this.show_details_modal = true this.show_details_modal = true
} }
},
watch: {
'state' () {
if (this.interval_id > 0) {
window.clearTimeout(this.interval_id)
this.interval_id = 0
}
this.item_progress_ms = this.state.item_progress_ms
if (this.state.state === 'play') {
this.interval_id = window.setInterval(this.tick, 1000)
}
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,22 +1,35 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<div class="title is-4">{{ playlist.name }}</div> <div class="title is-4">
{{ playlist.name }}
</div>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_playlist_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span> <span class="icon"><i class="mdi mdi-shuffle" /></span>
<span>Shuffle</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p> <p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
<list-tracks :tracks="tracks" :uris="uris"></list-tracks> <list-tracks :tracks="tracks" :uris="uris" />
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" :uris="uris" @close="show_playlist_details_modal = false" /> <modal-dialog-playlist
:show="show_playlist_details_modal"
:playlist="playlist"
:uris="uris"
@close="show_playlist_details_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -45,7 +58,20 @@ export default {
name: 'PagePlaylist', name: 'PagePlaylist',
components: { ContentWithHeading, ListTracks, ModalDialogPlaylist }, components: { ContentWithHeading, ListTracks, ModalDialogPlaylist },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
playlist: {}, playlist: {},
tracks: [], tracks: [],
@ -55,9 +81,9 @@ export default {
}, },
computed: { computed: {
uris () { uris() {
if (this.playlist.random) { if (this.playlist.random) {
return this.tracks.map(a => a.uri).join(',') return this.tracks.map((a) => a.uri).join(',')
} }
return this.playlist.uri return this.playlist.uri
} }
@ -67,22 +93,8 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.uris, true) webapi.player_play_uri(this.uris, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,11 +1,13 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">{{ playlist.name }}</p> <p class="title is-4">
{{ playlist.name }}
</p>
<p class="heading">{{ playlists.total }} playlists</p> <p class="heading">{{ playlists.total }} playlists</p>
</template> </template>
<template v-slot:content> <template #content>
<list-playlists :playlists="playlists.items"></list-playlists> <list-playlists :playlists="playlists.items" />
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -33,27 +35,26 @@ export default {
name: 'PagePlaylists', name: 'PagePlaylists',
components: { ContentWithHeading, ListPlaylists }, components: { ContentWithHeading, ListPlaylists },
data () { beforeRouteEnter(to, from, next) {
return {
playlist: {},
playlists: {}
}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },
beforeRouteUpdate (to, from, next) { beforeRouteUpdate(to, from, next) {
const vm = this const vm = this
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
dataObject.set(vm, response) dataObject.set(vm, response)
next() next()
}) })
},
data() {
return {
playlist: {},
playlists: {}
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,31 +1,46 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<div class="title is-4">{{ album.name }} <div class="title is-4">
{{ album.name }}
</div> </div>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true"> <a
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> class="button is-small is-light is-rounded"
@click="show_album_details_modal = true"
>
<span class="icon"
><i class="mdi mdi-dots-horizontal mdi-18px"
/></span>
</a> </a>
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"> <span class="icon">
<i class="mdi mdi-play"></i> <i class="mdi mdi-play" />
</span> </span>
<span>Play</span> <span>Play</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p> <p class="heading has-text-centered-mobile">
<list-item-track v-for="track in tracks" :key="track.id" :track="track" @click="play_track(track)"> {{ album.track_count }} tracks
<template v-slot:progress> </p>
<list-item-track
v-for="track in tracks"
:key="track.id"
:track="track"
@click="play_track(track)"
>
<template #progress>
<progress-bar :max="track.length_ms" :value="track.seek_ms" /> <progress-bar :max="track.length_ms" :value="track.seek_ms" />
</template> </template>
<template v-slot:actions> <template #actions>
<a @click.prevent.stop="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-track> </list-item-track>
@ -33,7 +48,8 @@
:show="show_details_modal" :show="show_details_modal"
:track="selected_track" :track="selected_track"
@close="show_details_modal = false" @close="show_details_modal = false"
@play-count-changed="reload_tracks" /> @play-count-changed="reload_tracks"
/>
<modal-dialog-album <modal-dialog-album
:show="show_album_details_modal" :show="show_album_details_modal"
:album="album" :album="album"
@ -41,16 +57,22 @@
:new_tracks="new_tracks" :new_tracks="new_tracks"
@close="show_album_details_modal = false" @close="show_album_details_modal = false"
@play-count-changed="reload_tracks" @play-count-changed="reload_tracks"
@remove-podcast="open_remove_podcast_dialog" /> @remove-podcast="open_remove_podcast_dialog"
/>
<modal-dialog <modal-dialog
:show="show_remove_podcast_modal" :show="show_remove_podcast_modal"
title="Remove podcast" title="Remove podcast"
delete_action="Remove" delete_action="Remove"
@close="show_remove_podcast_modal = false" @close="show_remove_podcast_modal = false"
@delete="remove_podcast"> @delete="remove_podcast"
<template v-slot:modal-content> >
<template #modal-content>
<p>Permanently remove this podcast from your library?</p> <p>Permanently remove this podcast from your library?</p>
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p> <p class="is-size-7">
(This will also remove the RSS playlist
<b>{{ rss_playlist_to_remove.name }}</b
>.)
</p>
</template> </template>
</modal-dialog> </modal-dialog>
</template> </template>
@ -91,7 +113,20 @@ export default {
ProgressBar ProgressBar
}, },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
album: {}, album: {},
tracks: [], tracks: [],
@ -107,8 +142,8 @@ export default {
}, },
computed: { computed: {
new_tracks () { new_tracks() {
return this.tracks.filter(track => track.play_count === 0).length return this.tracks.filter((track) => track.play_count === 0).length
} }
}, },
@ -129,9 +164,12 @@ export default {
open_remove_podcast_dialog: function () { open_remove_podcast_dialog: function () {
this.show_album_details_modal = false this.show_album_details_modal = false
webapi.library_track_playlists(this.tracks[0].id).then(({ data }) => { webapi.library_track_playlists(this.tracks[0].id).then(({ data }) => {
const rssPlaylists = data.items.filter(pl => pl.type === 'rss') const rssPlaylists = data.items.filter((pl) => pl.type === 'rss')
if (rssPlaylists.length !== 1) { if (rssPlaylists.length !== 1) {
this.$store.dispatch('add_notification', { text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.', type: 'danger' }) this.$store.dispatch('add_notification', {
text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.',
type: 'danger'
})
return return
} }
@ -142,9 +180,11 @@ export default {
remove_podcast: function () { remove_podcast: function () {
this.show_remove_podcast_modal = false this.show_remove_podcast_modal = false
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => { webapi
this.$router.replace({ path: '/podcasts' }) .library_playlist_delete(this.rss_playlist_to_remove.id)
}) .then(() => {
this.$router.replace({ path: '/podcasts' })
})
}, },
reload_tracks: function () { reload_tracks: function () {
@ -152,22 +192,8 @@ export default {
this.tracks = data.tracks.items this.tracks = data.tracks.items
}) })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,64 +1,78 @@
<template> <template>
<div> <div>
<content-with-heading v-if="new_episodes.items.length > 0"> <content-with-heading v-if="new_episodes.items.length > 0">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">New episodes</p> <p class="title is-4">New episodes</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small" @click="mark_all_played"> <a class="button is-small" @click="mark_all_played">
<span class="icon"> <span class="icon">
<i class="mdi mdi-pencil"></i> <i class="mdi mdi-pencil" />
</span> </span>
<span>Mark All Played</span> <span>Mark All Played</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<list-item-track v-for="track in new_episodes.items" :key="track.id" :track="track" @click="play_track(track)"> <list-item-track
<template v-slot:progress> v-for="track in new_episodes.items"
:key="track.id"
:track="track"
@click="play_track(track)"
>
<template #progress>
<progress-bar :max="track.length_ms" :value="track.seek_ms" /> <progress-bar :max="track.length_ms" :value="track.seek_ms" />
</template> </template>
<template v-slot:actions> <template #actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
</template> </template>
</list-item-track> </list-item-track>
<modal-dialog-track :show="show_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" @play-count-changed="reload_new_episodes" /> <modal-dialog-track
:show="show_track_details_modal"
:track="selected_track"
@close="show_track_details_modal = false"
@play-count-changed="reload_new_episodes"
/>
</template> </template>
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Podcasts</p> <p class="title is-4">Podcasts</p>
<p class="heading">{{ albums.total }} podcasts</p> <p class="heading">{{ albums.total }} podcasts</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a v-if="rss.tracks > 0" class="button is-small" @click="update_rss"> <a v-if="rss.tracks > 0" class="button is-small" @click="update_rss">
<span class="icon"> <span class="icon">
<i class="mdi mdi-refresh"></i> <i class="mdi mdi-refresh" />
</span> </span>
<span>Update</span> <span>Update</span>
</a> </a>
<a class="button is-small" @click="open_add_podcast_dialog"> <a class="button is-small" @click="open_add_podcast_dialog">
<span class="icon"> <span class="icon">
<i class="mdi mdi-rss"></i> <i class="mdi mdi-rss" />
</span> </span>
<span>Add Podcast</span> <span>Add Podcast</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="albums.items" <list-albums
@play-count-changed="reload_new_episodes()" :albums="albums.items"
@podcast-deleted="reload_podcasts()"> @play-count-changed="reload_new_episodes()"
</list-albums> @podcast-deleted="reload_podcasts()"
/>
<modal-dialog-add-rss <modal-dialog-add-rss
:show="show_url_modal" :show="show_url_modal"
@close="show_url_modal = false" @close="show_url_modal = false"
@podcast-added="reload_podcasts()" /> @podcast-added="reload_podcasts()"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -99,7 +113,20 @@ export default {
ProgressBar ProgressBar
}, },
data () { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return { return {
albums: { items: [] }, albums: { items: [] },
new_episodes: { items: [] }, new_episodes: { items: [] },
@ -112,7 +139,7 @@ export default {
}, },
computed: { computed: {
rss () { rss() {
return this.$store.state.rss_count return this.$store.state.rss_count
} }
}, },
@ -128,10 +155,10 @@ export default {
}, },
mark_all_played: function () { mark_all_played: function () {
this.new_episodes.items.forEach(ep => { this.new_episodes.items.forEach((ep) => {
webapi.library_track_update(ep.id, { play_count: 'increment' }) webapi.library_track_update(ep.id, { play_count: 'increment' })
}) })
this.new_episodes.items = { } this.new_episodes.items = {}
}, },
open_add_podcast_dialog: function (item) { open_add_podcast_dialog: function (item) {
@ -155,22 +182,8 @@ export default {
this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, 'rss') this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, 'rss')
this.$store.commit(types.SHOW_UPDATE_DIALOG, true) this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,66 +1,103 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="heading">{{ queue.count }} tracks</p> <p class="heading">{{ queue.count }} tracks</p>
<p class="title is-4">Queue</p> <p class="title is-4">Queue</p>
</template> </template>
<template v-slot:heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small" :class="{ 'is-info': show_only_next_items }" @click="update_show_next_items"> <a
class="button is-small"
:class="{ 'is-info': show_only_next_items }"
@click="update_show_next_items"
>
<span class="icon"> <span class="icon">
<i class="mdi mdi-arrow-collapse-down"></i> <i class="mdi mdi-arrow-collapse-down" />
</span> </span>
<span>Hide previous</span> <span>Hide previous</span>
</a> </a>
<a class="button is-small" @click="open_add_stream_dialog"> <a class="button is-small" @click="open_add_stream_dialog">
<span class="icon"> <span class="icon">
<i class="mdi mdi-web"></i> <i class="mdi mdi-web" />
</span> </span>
<span>Add Stream</span> <span>Add Stream</span>
</a> </a>
<a class="button is-small" :class="{ 'is-info': edit_mode }" @click="edit_mode = !edit_mode"> <a
class="button is-small"
:class="{ 'is-info': edit_mode }"
@click="edit_mode = !edit_mode"
>
<span class="icon"> <span class="icon">
<i class="mdi mdi-pencil"></i> <i class="mdi mdi-pencil" />
</span> </span>
<span>Edit</span> <span>Edit</span>
</a> </a>
<a class="button is-small" @click="queue_clear"> <a class="button is-small" @click="queue_clear">
<span class="icon"> <span class="icon">
<i class="mdi mdi-delete-empty"></i> <i class="mdi mdi-delete-empty" />
</span> </span>
<span>Clear</span> <span>Clear</span>
</a> </a>
<a class="button is-small" v-if="is_queue_save_allowed" :disabled="queue_items.length === 0" @click="save_dialog"> <a
v-if="is_queue_save_allowed"
class="button is-small"
:disabled="queue_items.length === 0"
@click="save_dialog"
>
<span class="icon"> <span class="icon">
<i class="mdi mdi-content-save"></i> <i class="mdi mdi-content-save" />
</span> </span>
<span>Save</span> <span>Save</span>
</a> </a>
</div> </div>
</template> </template>
<template v-slot:content> <template #content>
<draggable v-model="queue_items" handle=".handle" item-key="id" @end="move_item"> <draggable
v-model="queue_items"
handle=".handle"
item-key="id"
@end="move_item"
>
<template #item="{ element, index }"> <template #item="{ element, index }">
<list-item-queue-item <list-item-queue-item
:item="element" :item="element"
:position="index" :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"> :edit_mode="edit_mode"
<template v-slot:actions> >
<a @click.prevent.stop="open_dialog(element)" v-if="!edit_mode"> <template #actions>
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <a v-if="!edit_mode" @click.prevent.stop="open_dialog(element)">
</a> <span class="icon has-text-dark"
<a @click.prevent.stop="remove(element)" v-if="element.id !== state.item_id && edit_mode"> ><i class="mdi mdi-dots-vertical mdi-18px"
<span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span> /></span>
</a> </a>
</template> <a
</list-item-queue-item> v-if="element.id !== state.item_id && edit_mode"
@click.prevent.stop="remove(element)"
>
<span class="icon has-text-grey"
><i class="mdi mdi-delete mdi-18px"
/></span>
</a>
</template>
</list-item-queue-item>
</template> </template>
</draggable> </draggable>
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" /> <modal-dialog-queue-item
<modal-dialog-add-url-stream :show="show_url_modal" @close="show_url_modal = false" /> :show="show_details_modal"
<modal-dialog-playlist-save v-if="is_queue_save_allowed" :show="show_pls_save_modal" @close="show_pls_save_modal = false" /> :item="selected_item"
@close="show_details_modal = false"
/>
<modal-dialog-add-url-stream
:show="show_url_modal"
@close="show_url_modal = false"
/>
<modal-dialog-playlist-save
v-if="is_queue_save_allowed"
:show="show_pls_save_modal"
@close="show_pls_save_modal = false"
/>
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
@ -77,9 +114,16 @@ import draggable from 'vuedraggable'
export default { export default {
name: 'PageQueue', name: 'PageQueue',
components: { ContentWithHeading, ListItemQueueItem, draggable, ModalDialogQueueItem, ModalDialogAddUrlStream, ModalDialogPlaylistSave }, components: {
ContentWithHeading,
ListItemQueueItem,
draggable,
ModalDialogQueueItem,
ModalDialogAddUrlStream,
ModalDialogPlaylistSave
},
data () { data() {
return { return {
edit_mode: false, edit_mode: false,
@ -91,24 +135,33 @@ export default {
}, },
computed: { computed: {
state () { state() {
return this.$store.state.player return this.$store.state.player
}, },
is_queue_save_allowed () { is_queue_save_allowed() {
return this.$store.state.config.allow_modifying_stored_playlists && this.$store.state.config.default_playlist_directory return (
this.$store.state.config.allow_modifying_stored_playlists &&
this.$store.state.config.default_playlist_directory
)
}, },
queue () { queue() {
return this.$store.state.queue return this.$store.state.queue
}, },
queue_items: { queue_items: {
get () { return this.$store.state.queue.items }, get() {
set (value) { /* Do nothing? Send move request in @end event */ } return this.$store.state.queue.items
},
set(value) {
/* Do nothing? Send move request in @end event */
}
}, },
current_position () { current_position() {
const nowPlaying = this.$store.getters.now_playing const nowPlaying = this.$store.getters.now_playing
return nowPlaying === undefined || nowPlaying.position === undefined ? -1 : this.$store.getters.now_playing.position return nowPlaying === undefined || nowPlaying.position === undefined
? -1
: this.$store.getters.now_playing.position
}, },
show_only_next_items () { show_only_next_items() {
return this.$store.state.show_only_next_items return this.$store.state.show_only_next_items
} }
}, },
@ -127,7 +180,9 @@ export default {
}, },
move_item: function (e) { move_item: function (e) {
const oldPosition = !this.show_only_next_items ? e.oldIndex : e.oldIndex + this.current_position const oldPosition = !this.show_only_next_items
? e.oldIndex
: e.oldIndex + this.current_position
const item = this.queue_items[oldPosition] const item = this.queue_items[oldPosition]
const newPosition = item.position + (e.newIndex - e.oldIndex) const newPosition = item.position + (e.newIndex - e.oldIndex)
if (newPosition !== oldPosition) { if (newPosition !== oldPosition) {
@ -153,5 +208,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,12 +1,14 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Radio</p> <p class="title is-4">Radio</p>
</template> </template>
<template v-slot:content> <template #content>
<p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">
<list-tracks :tracks="tracks.items"></list-tracks> {{ tracks.total }} tracks
</p>
<list-tracks :tracks="tracks.items" />
</template> </template>
</content-with-heading> </content-with-heading>
</div> </div>
@ -31,26 +33,25 @@ export default {
name: 'PageRadioStreams', name: 'PageRadioStreams',
components: { ContentWithHeading, ListTracks }, components: { ContentWithHeading, ListTracks },
data () { beforeRouteEnter(to, from, next) {
return {
tracks: { items: [] }
}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },
beforeRouteUpdate (to, from, next) { beforeRouteUpdate(to, from, next) {
const vm = this const vm = this
dataObject.load(to).then((response) => { dataObject.load(to).then((response) => {
dataObject.set(vm, response) dataObject.set(vm, response)
next() next()
}) })
},
data() {
return {
tracks: { items: [] }
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -5,91 +5,122 @@
<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">
<form v-on:submit.prevent="new_search"> <form @submit.prevent="new_search">
<div class="field"> <div class="field">
<p class="control is-expanded has-icons-left"> <p class="control is-expanded has-icons-left">
<input class="input is-rounded is-shadowless" type="text" placeholder="Search" v-model="search_query" ref="search_field" autocomplete="off"> <input
ref="search_field"
v-model="search_query"
class="input is-rounded is-shadowless"
type="text"
placeholder="Search"
autocomplete="off"
/>
<span class="icon is-left"> <span class="icon is-left">
<i class="mdi mdi-magnify"></i> <i class="mdi mdi-magnify" />
</span> </span>
</p> </p>
<p class="help has-text-centered">Tip: you can search by a smart playlist query language <a href="https://github.com/owntone/owntone-server/blob/master/README_SMARTPL.md" target="_blank">expression</a> if you prefix it <p class="help has-text-centered">
with <code>query:</code>. Tip: you can search by a smart playlist query language
<a
href="https://github.com/owntone/owntone-server/blob/master/README_SMARTPL.md"
target="_blank"
>expression</a
>
if you prefix it with <code>query:</code>.
</p> </p>
</div> </div>
</form> </form>
<div class="tags" style="margin-top: 16px;"> <div class="tags" style="margin-top: 16px">
<a class="tag" v-for="recent_search in recent_searches" :key="recent_search" @click="open_recent_search(recent_search)">{{ recent_search }}</a> <a
v-for="recent_search in recent_searches"
:key="recent_search"
class="tag"
@click="open_recent_search(recent_search)"
>{{ recent_search }}</a
>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<tabs-search :query="search_query"></tabs-search> <tabs-search :query="search_query" />
<!-- Tracks --> <!-- Tracks -->
<content-with-heading v-if="show_tracks && tracks.total"> <content-with-heading v-if="show_tracks && tracks.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Tracks</p> <p class="title is-4">Tracks</p>
</template> </template>
<template v-slot:content> <template #content>
<list-tracks :tracks="tracks.items"></list-tracks> <list-tracks :tracks="tracks.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_tracks_button" class="level"> <nav v-if="show_all_tracks_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a> <a
class="button is-light is-small is-rounded"
@click="open_search_tracks"
>Show all {{ tracks.total.toLocaleString() }} tracks</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_tracks && !tracks.total" class="mt-6"> <content-text v-if="show_tracks && !tracks.total" class="mt-6">
<template v-slot:content> <template #content>
<p><i>No tracks found</i></p> <p><i>No tracks found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Artists --> <!-- Artists -->
<content-with-heading v-if="show_artists && artists.total"> <content-with-heading v-if="show_artists && artists.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
</template> </template>
<template v-slot:content> <template #content>
<list-artists :artists="artists.items"></list-artists> <list-artists :artists="artists.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_artists_button" class="level"> <nav v-if="show_all_artists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a> <a
class="button is-light is-small is-rounded"
@click="open_search_artists"
>Show all {{ artists.total.toLocaleString() }} artists</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_artists && !artists.total"> <content-text v-if="show_artists && !artists.total">
<template v-slot:content> <template #content>
<p><i>No artists found</i></p> <p><i>No artists found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Albums --> <!-- Albums -->
<content-with-heading v-if="show_albums && albums.total"> <content-with-heading v-if="show_albums && albums.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="albums.items"></list-albums> <list-albums :albums="albums.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_albums_button" class="level"> <nav v-if="show_all_albums_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a> <a
class="button is-light is-small is-rounded"
@click="open_search_albums"
>Show all {{ albums.total.toLocaleString() }} albums</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_albums && !albums.total"> <content-text v-if="show_albums && !albums.total">
<template v-slot:content> <template #content>
<p><i>No albums found</i></p> <p><i>No albums found</i></p>
</template> </template>
</content-text> </content-text>
@ -100,12 +131,16 @@
<p class="title is-4">Composers</p> <p class="title is-4">Composers</p>
</template> </template>
<template slot:content> <template slot:content>
<list-composers :composers="composers.items"></list-composers> <list-composers :composers="composers.items" />
</template> </template>
<template slot:footer> <template slot:footer>
<nav v-if="show_all_composers_button" class="level"> <nav v-if="show_all_composers_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_composers">Show all {{ composers.total }} composers</a> <a
class="button is-light is-small is-rounded"
@click="open_search_composers"
>Show all {{ composers.total }} composers</a
>
</p> </p>
</nav> </nav>
</template> </template>
@ -118,66 +153,78 @@
<!-- Playlists --> <!-- Playlists -->
<content-with-heading v-if="show_playlists && playlists.total"> <content-with-heading v-if="show_playlists && playlists.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Playlists</p> <p class="title is-4">Playlists</p>
</template> </template>
<template v-slot:content> <template #content>
<list-playlists :playlists="playlists.items"></list-playlists> <list-playlists :playlists="playlists.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_playlists_button" class="level"> <nav v-if="show_all_playlists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a> <a
class="button is-light is-small is-rounded"
@click="open_search_playlists"
>Show all {{ playlists.total.toLocaleString() }} playlists</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_playlists && !playlists.total"> <content-text v-if="show_playlists && !playlists.total">
<template v-slot:content> <template #content>
<p><i>No playlists found</i></p> <p><i>No playlists found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Podcasts --> <!-- Podcasts -->
<content-with-heading v-if="show_podcasts && podcasts.total"> <content-with-heading v-if="show_podcasts && podcasts.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Podcasts</p> <p class="title is-4">Podcasts</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="podcasts.items"></list-albums> <list-albums :albums="podcasts.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_podcasts_button" class="level"> <nav v-if="show_all_podcasts_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_podcasts">Show all {{ podcasts.total.toLocaleString() }} podcasts</a> <a
class="button is-light is-small is-rounded"
@click="open_search_podcasts"
>Show all {{ podcasts.total.toLocaleString() }} podcasts</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_podcasts && !podcasts.total"> <content-text v-if="show_podcasts && !podcasts.total">
<template v-slot:content> <template #content>
<p><i>No podcasts found</i></p> <p><i>No podcasts found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Audiobooks --> <!-- Audiobooks -->
<content-with-heading v-if="show_audiobooks && audiobooks.total"> <content-with-heading v-if="show_audiobooks && audiobooks.total">
<template v-slot:heading-left> <template #heading-left>
<p class="title is-4">Audiobooks</p> <p class="title is-4">Audiobooks</p>
</template> </template>
<template v-slot:content> <template #content>
<list-albums :albums="audiobooks.items"></list-albums> <list-albums :albums="audiobooks.items" />
</template> </template>
<template v-slot:footer> <template #footer>
<nav v-if="show_all_audiobooks_button" class="level"> <nav v-if="show_all_audiobooks_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_audiobooks">Show all {{ audiobooks.total.toLocaleString() }} audiobooks</a> <a
class="button is-light is-small is-rounded"
@click="open_search_audiobooks"
>Show all {{ audiobooks.total.toLocaleString() }} audiobooks</a
>
</p> </p>
</nav> </nav>
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_audiobooks && !audiobooks.total"> <content-text v-if="show_audiobooks && !audiobooks.total">
<template v-slot:content> <template #content>
<p><i>No audiobooks found</i></p> <p><i>No audiobooks found</i></p>
</template> </template>
</content-text> </content-text>
@ -198,9 +245,18 @@ import * as types from '@/store/mutation_types'
export default { export default {
name: 'PageSearch', name: 'PageSearch',
components: { ContentWithHeading, ContentText, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists, ListComposers }, components: {
ContentWithHeading,
ContentText,
TabsSearch,
ListTracks,
ListArtists,
ListAlbums,
ListPlaylists,
ListComposers
},
data () { data() {
return { return {
search_query: '', search_query: '',
@ -215,64 +271,85 @@ export default {
}, },
computed: { computed: {
recent_searches () { recent_searches() {
return this.$store.state.recent_searches return this.$store.state.recent_searches
}, },
show_tracks () { show_tracks() {
return this.$route.query.type && this.$route.query.type.includes('track') return this.$route.query.type && this.$route.query.type.includes('track')
}, },
show_all_tracks_button () { show_all_tracks_button() {
return this.tracks.total > this.tracks.items.length return this.tracks.total > this.tracks.items.length
}, },
show_artists () { show_artists() {
return this.$route.query.type && this.$route.query.type.includes('artist') return this.$route.query.type && this.$route.query.type.includes('artist')
}, },
show_all_artists_button () { show_all_artists_button() {
return this.artists.total > this.artists.items.length return this.artists.total > this.artists.items.length
}, },
show_albums () { show_albums() {
return this.$route.query.type && this.$route.query.type.includes('album') return this.$route.query.type && this.$route.query.type.includes('album')
}, },
show_all_albums_button () { show_all_albums_button() {
return this.albums.total > this.albums.items.length return this.albums.total > this.albums.items.length
}, },
show_composers () { show_composers() {
return this.$route.query.type && this.$route.query.type.includes('composer') return (
this.$route.query.type && this.$route.query.type.includes('composer')
)
}, },
show_all_composers_button () { show_all_composers_button() {
return this.composers.total > this.composers.items.length return this.composers.total > this.composers.items.length
}, },
show_playlists () { show_playlists() {
return this.$route.query.type && this.$route.query.type.includes('playlist') return (
this.$route.query.type && this.$route.query.type.includes('playlist')
)
}, },
show_all_playlists_button () { show_all_playlists_button() {
return this.playlists.total > this.playlists.items.length return this.playlists.total > this.playlists.items.length
}, },
show_audiobooks () { show_audiobooks() {
return this.$route.query.type && this.$route.query.type.includes('audiobook') return (
this.$route.query.type && this.$route.query.type.includes('audiobook')
)
}, },
show_all_audiobooks_button () { show_all_audiobooks_button() {
return this.audiobooks.total > this.audiobooks.items.length return this.audiobooks.total > this.audiobooks.items.length
}, },
show_podcasts () { show_podcasts() {
return this.$route.query.type && this.$route.query.type.includes('podcast') return (
this.$route.query.type && this.$route.query.type.includes('podcast')
)
}, },
show_all_podcasts_button () { show_all_podcasts_button() {
return this.podcasts.total > this.podcasts.items.length return this.podcasts.total > this.podcasts.items.length
}, },
is_visible_artwork () { is_visible_artwork() {
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value return this.$store.getters.settings_option(
'webinterface',
'show_cover_artwork_in_album_lists'
).value
} }
}, },
watch: {
$route(to, from) {
this.search(to)
}
},
mounted: function () {
this.search(this.$route)
},
methods: { methods: {
search: function (route) { search: function (route) {
if (!route.query.query || route.query.query === '') { if (!route.query.query || route.query.query === '') {
@ -289,7 +366,12 @@ export default {
}, },
searchMusic: function (query) { searchMusic: function (query) {
if (query.type.indexOf('track') < 0 && query.type.indexOf('artist') < 0 && query.type.indexOf('album') < 0 && query.type.indexOf('playlist') < 0) { if (
query.type.indexOf('track') < 0 &&
query.type.indexOf('artist') < 0 &&
query.type.indexOf('album') < 0 &&
query.type.indexOf('playlist') < 0
) {
return return
} }
@ -313,8 +395,12 @@ export default {
this.tracks = data.tracks ? data.tracks : { items: [], total: 0 } this.tracks = data.tracks ? data.tracks : { items: [], total: 0 }
this.artists = data.artists ? data.artists : { items: [], total: 0 } this.artists = data.artists ? data.artists : { items: [], total: 0 }
this.albums = data.albums ? data.albums : { items: [], total: 0 } this.albums = data.albums ? data.albums : { items: [], total: 0 }
this.composers = data.composers ? data.composers : { items: [], total: 0 } this.composers = data.composers
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 } ? data.composers
: { items: [], total: 0 }
this.playlists = data.playlists
? data.playlists
: { items: [], total: 0 }
}) })
}, },
@ -331,7 +417,12 @@ export default {
if (query.query.startsWith('query:')) { if (query.query.startsWith('query:')) {
searchParams.expression = query.query.replace(/^query:/, '').trim() searchParams.expression = query.query.replace(/^query:/, '').trim()
} else { } else {
searchParams.expression = '((album includes "' + query.query + '" or artist includes "' + query.query + '") and media_kind is audiobook)' searchParams.expression =
'((album includes "' +
query.query +
'" or artist includes "' +
query.query +
'") and media_kind is audiobook)'
} }
if (query.limit) { if (query.limit) {
@ -357,7 +448,12 @@ export default {
if (query.query.startsWith('query:')) { if (query.query.startsWith('query:')) {
searchParams.expression = query.query.replace(/^query:/, '').trim() searchParams.expression = query.query.replace(/^query:/, '').trim()
} else { } else {
searchParams.expression = '((album includes "' + query.query + '" or artist includes "' + query.query + '") and media_kind is podcast)' searchParams.expression =
'((album includes "' +
query.query +
'" or artist includes "' +
query.query +
'") and media_kind is podcast)'
} }
if (query.limit) { if (query.limit) {
@ -458,7 +554,10 @@ export default {
}, },
open_composer: function (composer) { open_composer: function (composer) {
this.$router.push({ name: 'ComposerAlbums', params: { composer: composer.name } }) this.$router.push({
name: 'ComposerAlbums',
params: { composer: composer.name }
})
}, },
open_playlist: function (playlist) { open_playlist: function (playlist) {
@ -494,19 +593,8 @@ export default {
this.selected_playlist = playlist this.selected_playlist = playlist
this.show_playlist_details_modal = true this.show_playlist_details_modal = true
} }
},
mounted: function () {
this.search(this.$route)
},
watch: {
'$route' (to, from) {
this.search(to)
}
} }
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,28 +1,50 @@
<template> <template>
<div class="fd-page-with-tabs"> <div class="fd-page-with-tabs">
<tabs-settings></tabs-settings> <tabs-settings />
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<div class="title is-4">Artwork</div> <div class="title is-4">Artwork</div>
</template> </template>
<template v-slot:content> <template #content>
<div class="content"> <div class="content">
<p> <p>
OwnTone supports PNG and JPEG artwork which is either placed as separate image files in the library, OwnTone supports PNG and JPEG artwork which is either placed as
embedded in the media files or made available online by radio stations. separate image files in the library, embedded in the media files or
made available online by radio stations.
</p>
<p>
In addition to that, you can enable fetching artwork from the
following artwork providers:
</p> </p>
<p>In addition to that, you can enable fetching artwork from the following artwork providers:</p>
</div> </div>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_spotify" v-if="spotify.libspotify_logged_in"> <settings-checkbox
<template v-slot:label> Spotify</template> v-if="spotify.libspotify_logged_in"
category_name="artwork"
option_name="use_artwork_source_spotify"
>
<template #label> Spotify </template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_discogs"> <settings-checkbox
<template v-slot:label> Discogs (<a href="https://www.discogs.com/">https://www.discogs.com/</a>)</template> category_name="artwork"
option_name="use_artwork_source_discogs"
>
<template #label>
Discogs (<a href="https://www.discogs.com/"
>https://www.discogs.com/</a
>)
</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_coverartarchive"> <settings-checkbox
<template v-slot:label> Cover Art Archive (<a href="https://coverartarchive.org/">https://coverartarchive.org/</a>)</template> category_name="artwork"
option_name="use_artwork_source_coverartarchive"
>
<template #label>
Cover Art Archive (<a href="https://coverartarchive.org/"
>https://coverartarchive.org/</a
>)
</template>
</settings-checkbox> </settings-checkbox>
</template> </template>
</content-with-heading> </content-with-heading>
@ -39,12 +61,11 @@ export default {
components: { ContentWithHeading, TabsSettings, SettingsCheckbox }, components: { ContentWithHeading, TabsSettings, SettingsCheckbox },
computed: { computed: {
spotify () { spotify() {
return this.$store.state.spotify return this.$store.state.spotify
} }
} }
} }
</script> </script>
<style> <style></style>
</style>

Some files were not shown because too many files have changed in this diff Show More