[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,11 +1,8 @@
module.exports = { module.exports = {
env: { env: {
node: true, node: true
}, },
extends: [ extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
'eslint:recommended',
'plugin:vue/vue3-recommended',
],
rules: { rules: {
// override/add rules settings here, such as: // override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error' // '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

@ -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,8 +37,13 @@ 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 {
@ -68,6 +80,15 @@ export default {
} }
}, },
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',
type: 'info',
topic: 'connection',
timeout: 2000
})
webapi.config().then(({ data }) => { webapi
.config()
.then(({ data }) => {
this.$store.commit(types.UPDATE_CONFIG, data) this.$store.commit(types.UPDATE_CONFIG, data)
this.$store.commit(types.HIDE_SINGLES, data.hide_singles) this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
document.title = data.library_name document.title = data.library_name
this.open_ws() this.open_ws()
this.$Progress.finish() this.$Progress.finish()
}).catch(() => { })
this.$store.dispatch('add_notification', { text: 'Failed to connect to OwnTone server', type: 'danger', topic: 'connection' }) .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

@ -18,10 +18,10 @@ 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
@ -31,8 +31,8 @@ export default {
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
}, },
@ -48,8 +48,14 @@ 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>
@ -31,7 +33,11 @@ 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)
}, },

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"
:key="option"
class="dropdown-item"
:class="{ 'is-active': modelValue === option }" :class="{ 'is-active': modelValue === option }"
@click="select(option)"> @click="select(option)"
>
{{ option }} {{ option }}
</a> </a>
</div> </div>
@ -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>
@ -15,7 +21,7 @@ export default {
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,64 +2,79 @@
<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
v-for="album in albums.grouped[idx]"
:key="album.id" :key="album.id"
class="media"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)"
<div class="media-left fd-has-action" >
v-if="is_visible_artwork"> <div v-if="is_visible_artwork" class="media-left fd-has-action">
<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>
</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">
<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>
</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
v-for="album in albums_list"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)"
<template v-slot:artwork v-if="is_visible_artwork"> >
<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>
@ -70,16 +85,22 @@
: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>
@ -111,7 +132,10 @@ 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,11 +175,16 @@ 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
.library_album_tracks(this.selected_album.id, { limit: 1 })
.then(({ data }) => {
webapi.library_track_playlists(data.items[0].id).then(({ data }) => { webapi.library_track_playlists(data.items[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
} }
@ -172,7 +201,9 @@ 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
.library_playlist_delete(this.rss_playlist_to_remove.id)
.then(() => {
this.$emit('podcast-deleted') this.$emit('podcast-deleted')
}) })
} }
@ -180,5 +211,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 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"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>
<list-item-artist
v-for="artist in artists.grouped[idx]"
:key="artist.id" :key="artist.id"
:artist="artist" :artist="artist"
@click="open_artist(artist)"> @click="open_artist(artist)"
<template v-slot:actions> >
<template #actions>
<a @click.prevent.stop="open_dialog(artist)"> <a @click.prevent.stop="open_dialog(artist)">
<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-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
v-for="artist in artists_list"
:key="artist.id" :key="artist.id"
:artist="artist" :artist="artist"
@click="open_artist(artist)"> @click="open_artist(artist)"
<template v-slot:actions> >
<template #actions>
<a @click.prevent.stop="open_dialog(artist)"> <a @click.prevent.stop="open_dialog(artist)">
<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-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>
@ -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"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>
<list-item-composer
v-for="composer in composers.grouped[idx]"
:key="composer.id" :key="composer.id"
:composer="composer" :composer="composer"
@click="open_composer(composer)"> @click="open_composer(composer)"
<template v-slot:actions> >
<template #actions>
<a @click.prevent.stop="open_dialog(composer)"> <a @click.prevent.stop="open_dialog(composer)">
<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-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
v-for="composer in composers_list"
:key="composer.id" :key="composer.id"
:composer="composer" :composer="composer"
@click="open_composer(composer)"> @click="open_composer(composer)"
<template v-slot:actions> >
<template #actions>
<a @click.prevent.stop="open_dialog(composer)"> <a @click.prevent.stop="open_dialog(composer)">
<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-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>
@ -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,7 +48,13 @@ 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() {
@ -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>
@ -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>
@ -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>
@ -55,21 +77,8 @@ export default {
} }
}, },
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>
@ -58,30 +77,8 @@ export default {
} }
}, },
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>
@ -123,14 +147,18 @@ 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
.library_album_track_update(this.album.id, { play_count: 'played' })
.then(({ data }) => {
this.$emit('play-count-changed') this.$emit('play-count-changed')
this.$emit('close') this.$emit('close')
}) })
@ -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>
@ -55,24 +73,8 @@ export default {
} }
}, },
methods: {
save: function () {
if (this.playlist_name.length < 1) {
return
}
this.loading = true
webapi.queue_save_playlist(this.playlist_name).then(() => {
this.$emit('close')
this.playlist_name = ''
}).catch(() => {
this.loading = false
})
}
},
watch: { watch: {
'show' () { show() {
if (this.show) { if (this.show) {
this.loading = false this.loading = false
@ -82,9 +84,27 @@ export default {
}, 10) }, 10)
} }
} }
},
methods: {
save: function () {
if (this.playlist_name.length < 1) {
return
}
this.loading = true
webapi
.queue_save_playlist(this.playlist_name)
.then(() => {
this.$emit('close')
this.playlist_name = ''
})
.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">
@ -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>
@ -92,6 +126,22 @@ export default {
} }
}, },
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>
@ -55,16 +71,8 @@ export default {
} }
}, },
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>
@ -114,6 +163,22 @@ export default {
} }
}, },
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
.library_track_update(this.track.id, { play_count: 'reset' })
.then(() => {
this.$emit('play-count-changed') this.$emit('play-count-changed')
this.$emit('close') this.$emit('close')
}) })
}, },
mark_played: function () { mark_played: function () {
webapi.library_track_update(this.track.id, { play_count: 'increment' }).then(() => { webapi
.library_track_update(this.track.id, { play_count: 'increment' })
.then(() => {
this.$emit('play-count-changed') this.$emit('play-count-changed')
this.$emit('close') 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

@ -5,25 +5,30 @@
: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>
@ -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"
@ -314,6 +447,24 @@ export default {
} }
}, },
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>

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="icon fd-has-action"
:class="{ 'has-text-grey-light': !output.selected }" :class="{ 'has-text-grey-light': !output.selected }"
v-on:click="set_enabled"> @click="set_enabled"
<i class="mdi mdi-18px" :class="type_class" :title="output.type"></i> >
<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"
@ -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="navbar-item has-dropdown is-hoverable"
:class="{ 'is-active': show_settings_menu }" :class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings"> @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>
@ -94,25 +148,46 @@ export default {
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() {
@ -169,19 +244,18 @@ export default {
} }
}, },
methods: {
on_click_outside_settings () {
this.show_settings_menu = !this.show_settings_menu
}
},
watch: { watch: {
$route(to, from) { $route(to, from) {
this.show_settings_menu = false this.show_settings_menu = false
} }
},
methods: {
on_click_outside_settings() {
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>

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>
@ -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>
@ -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>
@ -21,8 +32,10 @@ export default {
}, },
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() {
@ -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>
@ -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>
@ -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>
@ -19,8 +19,12 @@ export default {
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.$store.state.queue ||
this.$store.state.queue.count <= 0 ||
this.is_stopped ||
this.now_playing.data_kind === 'pipe' 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>
@ -19,8 +21,12 @@ export default {
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.$store.state.queue ||
this.$store.state.queue.count <= 0 ||
this.is_stopped ||
this.now_playing.data_kind === 'pipe' 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>
@ -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>
@ -10,7 +14,7 @@ export default {
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
}, },

View File

@ -1,19 +1,25 @@
<template> <template>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" <input
ref="settings_checkbox"
type="checkbox"
:checked="value" :checked="value"
@change="set_update_timer" @change="set_update_timer"
ref="settings_checkbox"> />
<slot name="label"></slot> <slot name="label" />
<i class="is-size-7" <i
class="is-size-7"
:class="{ :class="{
'has-text-info': statusUpdate === 'success', 'has-text-info': statusUpdate === 'success',
'has-text-danger': statusUpdate === 'error' 'has-text-danger': statusUpdate === 'error'
}"> {{ info }}</i> }"
>
{{ 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>
@ -39,14 +45,18 @@ 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() {
@ -92,13 +102,17 @@ export default {
name: this.option_name, name: this.option_name,
value: newValue value: newValue
} }
webapi.settings_update(this.category.name, option).then(() => { webapi
.settings_update(this.category.name, option)
.then(() => {
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'success' this.statusUpdate = 'success'
}).catch(() => { })
.catch(() => {
this.statusUpdate = 'error' this.statusUpdate = 'error'
this.$refs.settings_checkbox.checked = this.value this.$refs.settings_checkbox.checked = this.value
}).finally(() => { })
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
}) })
}, },
@ -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="is-size-7"
:class="{ :class="{
'has-text-info': statusUpdate === 'success', 'has-text-info': statusUpdate === 'success',
'has-text-danger': statusUpdate === 'error' 'has-text-danger': statusUpdate === 'error'
}"> {{ info }}</i> }"
>
{{ info }}</i
>
</label> </label>
<div class="control"> <div class="control">
<input class="input" <input
ref="settings_number"
class="input"
type="number" type="number"
min="0" min="0"
style="width: 10em;" style="width: 10em"
:placeholder="placeholder" :placeholder="placeholder"
:value="value" :value="value"
@input="set_update_timer" @input="set_update_timer"
ref="settings_number"> />
</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>
@ -46,14 +52,18 @@ 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() {
@ -98,13 +108,17 @@ 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
.settings_update(this.category.name, option)
.then(() => {
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'success' this.statusUpdate = 'success'
}).catch(() => { })
.catch(() => {
this.statusUpdate = 'error' this.statusUpdate = 'error'
this.$refs.settings_number.value = this.value this.$refs.settings_number.value = this.value
}).finally(() => { })
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
}) })
}, },
@ -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="is-size-7"
:class="{ :class="{
'has-text-info': statusUpdate === 'success', 'has-text-info': statusUpdate === 'success',
'has-text-danger': statusUpdate === 'error' 'has-text-danger': statusUpdate === 'error'
}"> {{ info }}</i> }"
>
{{ info }}</i
>
</label> </label>
<div class="control"> <div class="control">
<input class="input" type="text" :placeholder="placeholder" <input
ref="settings_text"
class="input"
type="text"
:placeholder="placeholder"
:value="value" :value="value"
@input="set_update_timer" @input="set_update_timer"
ref="settings_text"> />
</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>
@ -43,14 +51,18 @@ 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() {
@ -95,13 +107,17 @@ export default {
name: this.option_name, name: this.option_name,
value: newValue value: newValue
} }
webapi.settings_update(this.category.name, option).then(() => { webapi
.settings_update(this.category.name, option)
.then(() => {
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option) this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'success' this.statusUpdate = 'success'
}).catch(() => { })
.catch(() => {
this.statusUpdate = 'error' this.statusUpdate = 'error'
this.$refs.settings_text.value = this.value this.$refs.settings_text.value = this.value
}).finally(() => { })
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay) this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
}) })
}, },
@ -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>
@ -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
v-slot="{ navigate, isActive }"
to="/audiobooks/artists"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/audiobooks/albums"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/music/browse"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/music/artists"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/music/albums"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/music/genres"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/music/composers"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-if="spotify_enabled"
v-slot="{ navigate, isActive }"
to="/music/spotify"
custom
>
<li :class="{ 'is-active': isActive }"> <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>
@ -73,5 +108,4 @@ export default {
} }
</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>
@ -68,5 +72,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -5,28 +5,44 @@
<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
v-slot="{ navigate, isActive }"
to="/settings/webinterface"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/settings/remotes-outputs"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/settings/artwork"
custom
>
<li :class="{ 'is-active': isActive }"> <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
v-slot="{ navigate, isActive }"
to="/settings/online-services"
custom
>
<li :class="{ 'is-active': isActive }"> <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>
@ -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 = {}
@ -36,11 +43,14 @@ export default class Albums {
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)
@ -57,17 +67,29 @@ export default class Albums {
} }
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) {
@ -98,7 +120,7 @@ export default class Albums {
} }
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 = {}
@ -24,7 +31,10 @@ export default class Artists {
} }
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') {
@ -34,17 +44,28 @@ export default class Artists {
} }
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
} }
@ -55,7 +76,7 @@ export default class Artists {
} }
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 = {}
@ -24,7 +31,10 @@ export default class Composers {
} }
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') {
@ -34,17 +44,30 @@ export default class Composers {
} }
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
} }
@ -55,7 +78,7 @@ export default class Composers {
} }
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,7 +6,6 @@
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(/#/, '')
@ -14,11 +13,7 @@ function is_background_light (background_color) {
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
} }
@ -28,21 +23,42 @@ function calc_text_color (background_color) {
} }
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 =
'<svg width="' +
data.width +
'" height="' +
data.height +
'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' +
data.width +
' ' +
data.height +
'" preserveAspectRatio="none">' +
'<defs>' + '<defs>' +
'<style type="text/css">' + '<style type="text/css">' +
' #holder text {' + ' #holder text {' +
' fill: ' + data.textColor + ';' + ' fill: ' +
' font-family: ' + data.fontFamily + ';' + data.textColor +
' font-size: ' + data.fontSize + 'px;' + ';' +
' font-weight: ' + data.fontWeight + ';' + ' font-family: ' +
data.fontFamily +
';' +
' font-size: ' +
data.fontSize +
'px;' +
' font-weight: ' +
data.fontWeight +
';' +
' }' + ' }' +
' </style>' + ' </style>' +
'</defs>' + '</defs>' +
'<g id="holder">' + '<g id="holder">' +
' <rect width="100%" height="100%" fill="' + data.backgroundColor + '"></rect>' + ' <rect width="100%" height="100%" fill="' +
data.backgroundColor +
'"></rect>' +
' <g>' + ' <g>' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' + data.caption + '</text>' + ' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' +
data.caption +
'</text>' +
' </g>' + ' </g>' +
'</g>' + '</g>' +
'</svg>' '</svg>'

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,7 +278,6 @@ hr.fd-navbar-divider {
overflow: scroll; overflow: scroll;
} }
.buttons { .buttons {
@include mobile { @include mobile {
&.fd-is-centered-mobile { &.fd-is-centered-mobile {

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>
@ -111,5 +154,4 @@ export default {
} }
</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,6 +72,19 @@ export default {
name: 'PageAlbum', name: 'PageAlbum',
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
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() { data() {
return { return {
album: {}, album: {},
@ -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,15 +77,44 @@ 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
},
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() { data() {
return { return {
@ -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> </div>
</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">{{ 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,7 +71,25 @@ const dataObject = {
export default { export default {
name: 'PageArtist', name: 'PageArtist',
components: { ContentWithHeading, ListAlbums, ModalDialogArtist, DropdownMenu }, components: {
ContentWithHeading,
ListAlbums,
ModalDialogArtist,
DropdownMenu
},
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() { data() {
return { return {
@ -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,7 +66,25 @@ const dataObject = {
export default { export default {
name: 'PageArtistTracks', name: 'PageArtistTracks',
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist }, components: {
ContentWithHeading,
ListTracks,
IndexButtonList,
ModalDialogArtist
},
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() { data() {
return { return {
@ -62,12 +97,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()
)
)
]
}, },
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,7 +84,30 @@ const dataObject = {
export default { export default {
name: 'PageArtists', name: 'PageArtists',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu }, components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListArtists,
DropdownMenu
},
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() { data() {
return { return {
@ -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,6 +73,19 @@ export default {
name: 'PageAudiobooksAlbum', name: 'PageAudiobooksAlbum',
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
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() { data() {
return { return {
album: {}, album: {},
@ -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,7 +39,25 @@ const dataObject = {
export default { export default {
name: 'PageAudiobooksAlbums', name: 'PageAudiobooksAlbums',
components: { TabsAudiobooks, ContentWithHeading, IndexButtonList, ListAlbums }, components: {
TabsAudiobooks,
ContentWithHeading,
IndexButtonList,
ListAlbums
},
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() { data() {
return { return {
@ -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,6 +59,19 @@ export default {
name: 'PageAudiobooksArtist', name: 'PageAudiobooksArtist',
components: { ContentWithHeading, ListAlbums, ModalDialogArtist }, components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
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() { data() {
return { return {
artist: {}, artist: {},
@ -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,7 +40,25 @@ const dataObject = {
export default { export default {
name: 'PageAudiobooksArtists', name: 'PageAudiobooksArtists',
components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists }, components: {
ContentWithHeading,
TabsAudiobooks,
IndexButtonList,
ListArtists
},
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() { data() {
return { return {
@ -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,6 +83,19 @@ export default {
name: 'PageBrowse', name: 'PageBrowse',
components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks }, components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks },
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() { data() {
return { return {
recently_added: { items: [] }, recently_added: { 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,6 +42,19 @@ export default {
name: 'PageBrowseType', name: 'PageBrowseType',
components: { ContentWithHeading, TabsMusic, ListAlbums }, components: { ContentWithHeading, TabsMusic, ListAlbums },
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() { data() {
return { return {
recently_added: { items: [] } recently_added: { items: [] }
@ -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,15 +39,9 @@ export default {
name: 'PageBrowseType', name: 'PageBrowseType',
components: { ContentWithHeading, TabsMusic, ListTracks }, components: { ContentWithHeading, TabsMusic, ListTracks },
data () {
return {
recently_played: {}
}
},
beforeRouteEnter(to, from, next) { 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) {
@ -55,9 +50,14 @@ export default {
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,7 +79,25 @@ const dataObject = {
export default { export default {
name: 'PageComposer', name: 'PageComposer',
components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer }, components: {
ContentWithHeading,
ListItemAlbums,
ModalDialogAlbum,
ModalDialogComposer
},
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() { data() {
return { return {
@ -68,19 +112,30 @@ 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,7 +79,25 @@ const dataObject = {
export default { export default {
name: 'PageComposerTracks', name: 'PageComposerTracks',
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogComposer }, components: {
ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ModalDialogComposer
},
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() { data() {
return { return {
@ -71,27 +115,44 @@ 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,6 +47,19 @@ export default {
name: 'PageComposers', name: 'PageComposers',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers },
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() { data() {
return { return {
composers: { items: [] }, composers: { items: [] },
@ -57,8 +72,13 @@ 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() {
@ -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"
:key="directory.path"
:directory="directory"
@click="open_directory(directory)"
>
<template #actions>
<a @click="open_directory_dialog(directory)"> <a @click="open_directory_dialog(directory)">
<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-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
},
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() { 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: {},
@ -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,7 +61,25 @@ const dataObject = {
export default { export default {
name: 'PageGenre', name: 'PageGenre',
components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre }, components: {
ContentWithHeading,
IndexButtonList,
ListAlbums,
ModalDialogGenre
},
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() { data() {
return { return {
@ -59,8 +92,13 @@ 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,7 +61,25 @@ const dataObject = {
export default { export default {
name: 'PageGenreTracks', name: 'PageGenreTracks',
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre }, components: {
ContentWithHeading,
ListTracks,
IndexButtonList,
ModalDialogGenre
},
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() { data() {
return { return {
@ -59,8 +92,13 @@ 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() {
@ -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,7 +55,26 @@ const dataObject = {
export default { export default {
name: 'PageGenres', name: 'PageGenres',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre }, components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListItemGenre,
ModalDialogGenre
},
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() { data() {
return { return {
@ -57,8 +87,11 @@ 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,7 +49,10 @@
<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
v-if="composer"
class="subtitle is-6 has-text-grey has-text-weight-bold"
>
{{ composer }} {{ composer }}
</h2> </h2>
<h3 class="subtitle is-6"> <h3 class="subtitle is-6">
@ -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>
@ -94,23 +107,6 @@ export default {
} }
}, },
created () {
this.item_progress_ms = this.state.item_progress_ms
webapi.player_status().then(({ data }) => {
this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
if (this.state.state === 'play') {
this.interval_id = window.setInterval(this.tick, 1000)
}
})
},
destroyed () {
if (this.interval_id > 0) {
window.clearTimeout(this.interval_id)
this.interval_id = 0
}
},
computed: { computed: {
state() { state() {
return this.$store.state.player return this.$store.state.player
@ -130,11 +126,17 @@ export default {
composer() { composer() {
if (this.settings_option_show_composer_now_playing) { if (this.settings_option_show_composer_now_playing) {
if (!this.settings_option_show_composer_for_genre || if (
!this.settings_option_show_composer_for_genre ||
(this.now_playing.genre && (this.now_playing.genre &&
this.settings_option_show_composer_for_genre.toLowerCase() this.settings_option_show_composer_for_genre
.toLowerCase()
.split(',') .split(',')
.findIndex(elem => this.now_playing.genre.toLowerCase().indexOf(elem.trim()) >= 0) >= 0)) { .findIndex(
(elem) =>
this.now_playing.genre.toLowerCase().indexOf(elem.trim()) >= 0
) >= 0)
) {
return this.now_playing.composer return this.now_playing.composer
} }
} }
@ -142,6 +144,36 @@ export default {
} }
}, },
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
webapi.player_status().then(({ data }) => {
this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
if (this.state.state === 'play') {
this.interval_id = window.setInterval(this.tick, 1000)
}
})
},
unmounted() {
if (this.interval_id > 0) {
window.clearTimeout(this.interval_id)
this.interval_id = 0
}
},
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,6 +58,19 @@ export default {
name: 'PagePlaylist', name: 'PagePlaylist',
components: { ContentWithHeading, ListTracks, ModalDialogPlaylist }, components: { ContentWithHeading, ListTracks, ModalDialogPlaylist },
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() { data() {
return { return {
playlist: {}, playlist: {},
@ -57,7 +83,7 @@ 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,16 +35,9 @@ export default {
name: 'PagePlaylists', name: 'PagePlaylists',
components: { ContentWithHeading, ListPlaylists }, components: { ContentWithHeading, ListPlaylists },
data () {
return {
playlist: {},
playlists: {}
}
},
beforeRouteEnter(to, from, next) { 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) {
@ -51,9 +46,15 @@ export default {
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,6 +113,19 @@ export default {
ProgressBar ProgressBar
}, },
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() { data() {
return { return {
album: {}, album: {},
@ -108,7 +143,7 @@ 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,7 +180,9 @@ 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
.library_playlist_delete(this.rss_playlist_to_remove.id)
.then(() => {
this.$router.replace({ path: '/podcasts' }) this.$router.replace({ path: '/podcasts' })
}) })
}, },
@ -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
:albums="albums.items"
@play-count-changed="reload_new_episodes()" @play-count-changed="reload_new_episodes()"
@podcast-deleted="reload_podcasts()"> @podcast-deleted="reload_podcasts()"
</list-albums> />
<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,6 +113,19 @@ export default {
ProgressBar ProgressBar
}, },
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() { data() {
return { return {
albums: { items: [] }, albums: { items: [] },
@ -128,7 +155,7 @@ 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 = {}
@ -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)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a> </a>
<a @click.prevent.stop="remove(element)" v-if="element.id !== state.item_id && edit_mode"> <a
<span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span> 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> </a>
</template> </template>
</list-item-queue-item> </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,7 +114,14 @@ 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 {
@ -95,18 +139,27 @@ export default {
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,15 +33,9 @@ export default {
name: 'PageRadioStreams', name: 'PageRadioStreams',
components: { ContentWithHeading, ListTracks }, components: { ContentWithHeading, ListTracks },
data () {
return {
tracks: { items: [] }
}
},
beforeRouteEnter(to, from, next) { 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) {
@ -48,9 +44,14 @@ export default {
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,7 +245,16 @@ 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 {
@ -241,38 +297,59 @@ export default {
}, },
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>
@ -46,5 +68,4 @@ export default {
} }
</script> </script>
<style> <style></style>
</style>

View File

@ -1,19 +1,27 @@
<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">Spotify</div> <div class="title is-4">Spotify</div>
</template> </template>
<template v-slot:content> <template #content>
<div class="notification is-size-7" v-if="!spotify.spotify_installed"> <div v-if="!spotify.spotify_installed" class="notification is-size-7">
<p>OwnTone was either built without support for Spotify or libspotify is not installed.</p> <p>
OwnTone was either built without support for Spotify or libspotify
is not installed.
</p>
</div> </div>
<div v-if="spotify.spotify_installed"> <div v-if="spotify.spotify_installed">
<div class="notification is-size-7"> <div class="notification is-size-7">
<b>You must have a Spotify premium account</b>. <span v-if="use_libspotity">If you normally log into Spotify with your Facebook account you must first go to Spotify's web site where you can get the Spotify username and password that matches your account.</span> <b>You must have a Spotify premium account</b>.
<span v-if="use_libspotity"
>If you normally log into Spotify with your Facebook account you
must first go to Spotify's web site where you can get the Spotify
username and password that matches your account.</span
>
</div> </div>
<div v-if="use_libspotity"> <div v-if="use_libspotity">
@ -21,29 +29,53 @@
<b>libspotify</b> - Login with your Spotify username and password <b>libspotify</b> - Login with your Spotify username and password
</p> </p>
<p v-if="spotify.libspotify_logged_in" class="fd-has-margin-bottom"> <p v-if="spotify.libspotify_logged_in" class="fd-has-margin-bottom">
Logged in as <b><code>{{ spotify.libspotify_user }}</code></b> Logged in as
<b
><code>{{ spotify.libspotify_user }}</code></b
>
</p> </p>
<form v-if="spotify.spotify_installed && !spotify.libspotify_logged_in" @submit.prevent="login_libspotify"> <form
v-if="spotify.spotify_installed && !spotify.libspotify_logged_in"
@submit.prevent="login_libspotify"
>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="text" placeholder="Username" v-model="libspotify.user"> <input
<p class="help is-danger">{{ libspotify.errors.user }}</p> v-model="libspotify.user"
class="input"
type="text"
placeholder="Username"
/>
<p class="help is-danger">
{{ libspotify.errors.user }}
</p>
</div> </div>
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="password" placeholder="Password" v-model="libspotify.password"> <input
<p class="help is-danger">{{ libspotify.errors.password }}</p> v-model="libspotify.password"
class="input"
type="password"
placeholder="Password"
/>
<p class="help is-danger">
{{ libspotify.errors.password }}
</p>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-info">Login</button> <button class="button is-info">Login</button>
</div> </div>
</div> </div>
</form> </form>
<p class="help is-danger">{{ libspotify.errors.error }}</p> <p class="help is-danger">
{{ libspotify.errors.error }}
</p>
<p class="help"> <p class="help">
libspotify enables OwnTone to play Spotify tracks. libspotify enables OwnTone to play Spotify tracks.
</p> </p>
<p class="help"> <p class="help">
OwnTone will not store your password, but will still be able to log you in automatically afterwards, because libspotify saves a login token. OwnTone will not store your password, but will still be able to
log you in automatically afterwards, because libspotify saves a
login token.
</p> </p>
</div> </div>
@ -52,22 +84,42 @@
<b>Spotify Web API</b> - Grant access to the Spotify Web API <b>Spotify Web API</b> - Grant access to the Spotify Web API
</p> </p>
<p v-if="spotify.webapi_token_valid"> <p v-if="spotify.webapi_token_valid">
Access granted for <b><code>{{ spotify.webapi_user }}</code></b> Access granted for
<b
><code>{{ spotify.webapi_user }}</code></b
>
</p> </p>
<p class="help is-danger" v-if="spotify_missing_scope.length > 0"> <p v-if="spotify_missing_scope.length > 0" class="help is-danger">
Please reauthorize Web API access to grant OwnTone the following additional access rights: Please reauthorize Web API access to grant OwnTone the following
<b><code>{{ spotify_missing_scope.join() }}</code></b> additional access rights:
<b
><code>{{ spotify_missing_scope.join() }}</code></b
>
</p> </p>
<div class="field fd-has-margin-top"> <div class="field fd-has-margin-top">
<div class="control"> <div class="control">
<a class="button" :class="{ 'is-info': !spotify.webapi_token_valid || spotify_missing_scope.length > 0 }" :href="spotify.oauth_uri">Authorize Web API access</a> <a
class="button"
:class="{
'is-info':
!spotify.webapi_token_valid ||
spotify_missing_scope.length > 0
}"
:href="spotify.oauth_uri"
>Authorize Web API access</a
>
</div> </div>
</div> </div>
<p class="help"> <p class="help">
Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are Access to the Spotify Web API enables scanning of your Spotify
<code>{{ spotify_required_scope.join() }}</code>. library. Required scopes are
<code>{{ spotify_required_scope.join() }}</code
>.
</p> </p>
<div v-if="spotify.webapi_token_valid" class="field fd-has-margin-top "> <div
v-if="spotify.webapi_token_valid"
class="field fd-has-margin-top"
>
<div class="control"> <div class="control">
<a class="button is-danger" @click="logout_spotify">Logout</a> <a class="button is-danger" @click="logout_spotify">Logout</a>
</div> </div>
@ -78,17 +130,18 @@
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template v-slot:heading-left> <template #heading-left>
<div class="title is-4">Last.fm</div> <div class="title is-4">Last.fm</div>
</template> </template>
<template v-slot:content> <template #content>
<div class="notification is-size-7" v-if="!lastfm.enabled"> <div v-if="!lastfm.enabled" class="notification is-size-7">
<p>OwnTone was built without support for Last.fm.</p> <p>OwnTone was built without support for Last.fm.</p>
</div> </div>
<div v-if="lastfm.enabled"> <div v-if="lastfm.enabled">
<p class="content"> <p class="content">
<b>Last.fm</b> - Login with your Last.fm username and password to enable scrobbling <b>Last.fm</b> - Login with your Last.fm username and password to
enable scrobbling
</p> </p>
<div v-if="lastfm.scrobbling_enabled"> <div v-if="lastfm.scrobbling_enabled">
<a class="button" @click="logoutLastfm">Stop scrobbling</a> <a class="button" @click="logoutLastfm">Stop scrobbling</a>
@ -97,20 +150,37 @@
<form @submit.prevent="login_lastfm"> <form @submit.prevent="login_lastfm">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="text" placeholder="Username" v-model="lastfm_login.user"> <input
<p class="help is-danger">{{ lastfm_login.errors.user }}</p> v-model="lastfm_login.user"
class="input"
type="text"
placeholder="Username"
/>
<p class="help is-danger">
{{ lastfm_login.errors.user }}
</p>
</div> </div>
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="password" placeholder="Password" v-model="lastfm_login.password"> <input
<p class="help is-danger">{{ lastfm_login.errors.password }}</p> v-model="lastfm_login.password"
class="input"
type="password"
placeholder="Password"
/>
<p class="help is-danger">
{{ lastfm_login.errors.password }}
</p>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-info" type="submit">Login</button> <button class="button is-info" type="submit">Login</button>
</div> </div>
</div> </div>
<p class="help is-danger">{{ lastfm_login.errors.error }}</p> <p class="help is-danger">
{{ lastfm_login.errors.error }}
</p>
<p class="help"> <p class="help">
OwnTone will not store your Last.fm username/password, only the session key. The session key does not expire. OwnTone will not store your Last.fm username/password, only the
session key. The session key does not expire.
</p> </p>
</form> </form>
</div> </div>
@ -129,10 +199,24 @@ export default {
name: 'SettingsPageOnlineServices', name: 'SettingsPageOnlineServices',
components: { ContentWithHeading, TabsSettings }, components: { ContentWithHeading, TabsSettings },
filters: {
join(array) {
return array.join(', ')
}
},
data() { data() {
return { return {
libspotify: { user: '', password: '', errors: { user: '', password: '', error: '' } }, libspotify: {
lastfm_login: { user: '', password: '', errors: { user: '', password: '', error: '' } } user: '',
password: '',
errors: { user: '', password: '', error: '' }
},
lastfm_login: {
user: '',
password: '',
errors: { user: '', password: '', error: '' }
}
} }
}, },
@ -153,8 +237,16 @@ export default {
}, },
spotify_missing_scope() { spotify_missing_scope() {
if (this.spotify.webapi_token_valid && this.spotify.webapi_granted_scope && this.spotify.webapi_required_scope) { if (
return this.spotify.webapi_required_scope.split(' ').filter(scope => this.spotify.webapi_granted_scope.indexOf(scope) < 0) this.spotify.webapi_token_valid &&
this.spotify.webapi_granted_scope &&
this.spotify.webapi_required_scope
) {
return this.spotify.webapi_required_scope
.split(' ')
.filter(
(scope) => this.spotify.webapi_granted_scope.indexOf(scope) < 0
)
} }
return [] return []
}, },
@ -166,7 +258,7 @@ export default {
methods: { methods: {
login_libspotify() { login_libspotify() {
webapi.spotify_login(this.libspotify).then(response => { webapi.spotify_login(this.libspotify).then((response) => {
this.libspotify.user = '' this.libspotify.user = ''
this.libspotify.password = '' this.libspotify.password = ''
this.libspotify.errors.user = '' this.libspotify.errors.user = ''
@ -186,7 +278,7 @@ export default {
}, },
login_lastfm() { login_lastfm() {
webapi.lastfm_login(this.lastfm_login).then(response => { webapi.lastfm_login(this.lastfm_login).then((response) => {
this.lastfm_login.user = '' this.lastfm_login.user = ''
this.lastfm_login.password = '' this.lastfm_login.password = ''
this.lastfm_login.errors.user = '' this.lastfm_login.errors.user = ''
@ -204,15 +296,8 @@ export default {
logoutLastfm() { logoutLastfm() {
webapi.lastfm_logout() webapi.lastfm_logout()
} }
},
filters: {
join (array) {
return array.join(', ')
}
} }
} }
</script> </script>
<style> <style></style>
</style>

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