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

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

View File

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

View File

@ -3,6 +3,7 @@
- Vue Dev Tools required in version 6 (currently only released as beta versions): <https://devtools.vuejs.org/guide/installation.html#beta>
- [ ] 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://github.com/vitejs/vite/issues/1930>
@ -13,6 +14,7 @@
- [x] Update dialog is missing scan options
- [ ] Performance with huge artists/albums/tracks list (no functional template supported any more)
- [ ] Do not reload data, if using the index-nav
- [x] PageAlbums
- [ ] PageArtists
@ -21,54 +23,64 @@
- [ ] Evaluate virtual scroller <https://github.com/Akryum/vue-virtual-scroller/tree/next/packages/vue-virtual-scroller>
- [x] JS error on Podacst page
- Problem caused by the Slider component
- Replace with plain html
- [ ] vue-router scroll-behavior
- [x] Index list not always hidden
- [x] Check transitions
- [ ] Page display is "jumpy"
- Workaround is removing the page transition effect
- [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] Mobile view still broken
- [x] Update to latest dependency versions (vite, vue, etc.)
- [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
- [x] `vue-range-slider` is not compatible with vue3
- replacement option: <https://github.com/vueform/slider>
- [x] `@vueform/slider` for volume control
- [x] track progress (now playing)
- [x] track progress (podcasts)
- [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>
- 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-tiny-lazyload-img` does not support Vue 3
- 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
- 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>)
- Works as a directive
- Easy replacement for `vue-tiny-lazyload-img`
- [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
- [x] Mobile view seems to be broken
- 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] Replace unmaintained `vue-infinite-loading` dependency
- Replace with `@ts-pro/vue-eternal-loading`: <https://github.com/ts-pro/vue-eternal-loading>
- [x] Replace `bulma-switch` with `@vueform/toggle`?

View File

@ -3,13 +3,17 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?ver2.0">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<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">
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png?ver2.0"
/>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<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" />
<title>OwnTone</title>
</head>

View File

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

View File

@ -6,14 +6,21 @@
<component :is="Component" class="fd-page" />
</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
:show="show_update_dialog"
@close="show_update_dialog = false" />
:show="show_update_dialog"
@close="show_update_dialog = false"
/>
<notifications v-show="!show_burger_menu" />
<navbar-bottom />
<div class="fd-overlay-fullscreen" v-show="show_burger_menu || show_player_menu"
@click="show_burger_menu = show_player_menu = false"></div>
<div
v-show="show_burger_menu || show_player_menu"
class="fd-overlay-fullscreen"
@click="show_burger_menu = show_player_menu = false"
/>
</div>
</template>
@ -30,10 +37,15 @@ import moment from 'moment'
export default {
name: 'App',
components: { NavbarTop, NavbarBottom, Notifications, ModalDialogRemotePairing, ModalDialogUpdate },
template: '<App/>',
components: {
NavbarTop,
NavbarBottom,
Notifications,
ModalDialogRemotePairing,
ModalDialogUpdate
},
data () {
data() {
return {
token_timer_id: 0,
reconnect_attempts: 0,
@ -43,31 +55,40 @@ export default {
computed: {
show_burger_menu: {
get () {
get() {
return this.$store.state.show_burger_menu
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_BURGER_MENU, value)
}
},
show_player_menu: {
get () {
get() {
return this.$store.state.show_player_menu
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_PLAYER_MENU, value)
}
},
show_update_dialog: {
get () {
get() {
return this.$store.state.show_update_dialog
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
}
}
},
watch: {
show_burger_menu() {
this.update_is_clipped()
},
show_player_menu() {
this.update_is_clipped()
}
},
created: function () {
moment.locale(navigator.language)
this.connect()
@ -97,23 +118,38 @@ export default {
methods: {
connect: function () {
this.$store.dispatch('add_notification', { text: 'Connecting to OwnTone server', type: 'info', topic: 'connection', timeout: 2000 })
webapi.config().then(({ data }) => {
this.$store.commit(types.UPDATE_CONFIG, data)
this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
document.title = data.library_name
this.open_ws()
this.$Progress.finish()
}).catch(() => {
this.$store.dispatch('add_notification', { text: 'Failed to connect to OwnTone server', type: 'danger', topic: 'connection' })
this.$store.dispatch('add_notification', {
text: 'Connecting to OwnTone server',
type: 'info',
topic: 'connection',
timeout: 2000
})
webapi
.config()
.then(({ data }) => {
this.$store.commit(types.UPDATE_CONFIG, data)
this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
document.title = data.library_name
this.open_ws()
this.$Progress.finish()
})
.catch(() => {
this.$store.dispatch('add_notification', {
text: 'Failed to connect to OwnTone server',
type: 'danger',
topic: 'connection'
})
})
},
open_ws: function () {
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
}
@ -124,22 +160,47 @@ export default {
protocol = 'wss://'
}
let wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port
if (import.meta.env.NODE_ENV === 'development' && import.meta.env.VUE_APP_WEBSOCKET_SERVER) {
let wsUrl =
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
wsUrl = import.meta.env.VUE_APP_WEBSOCKET_SERVER
}
const socket = new ReconnectingWebSocket(
wsUrl,
'notify',
{ reconnectInterval: 3000 }
)
const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
reconnectInterval: 3000
})
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
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_player_status()
@ -155,14 +216,26 @@ export default {
}
socket.onerror = function () {
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) {
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()
}
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()
}
if (data.notify.includes('outputs') || data.notify.includes('volume')) {
@ -237,7 +310,10 @@ export default {
this.token_timer_id = 0
}
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 {
}
}
},
watch: {
'show_burger_menu' () {
this.update_is_clipped()
},
'show_player_menu' () {
this.update_is_clipped()
}
}
template: '<App/>'
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
<template>
<section>
<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>
<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
>
</nav>
</section>
</template>
@ -13,9 +19,9 @@ export default {
props: ['index'],
computed: {
filtered_index () {
filtered_index() {
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>
<style>
</style>
<style></style>

View File

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

View File

@ -2,32 +2,49 @@
<div>
<div v-if="is_grouped">
<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>
<list-item-artist v-for="artist in artists.grouped[idx]"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)">
<template v-slot:actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
<span
: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"
:artist="artist"
@click="open_artist(artist)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist>
</div>
</div>
<div v-else>
<list-item-artist v-for="artist in artists_list"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)">
<template v-slot:actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
<list-item-artist
v-for="artist in artists_list"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist>
</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>
</template>
@ -42,7 +59,7 @@ export default {
props: ['artists', 'media_kind'],
data () {
data() {
return {
show_details_modal: false,
selected_artist: {}
@ -62,7 +79,7 @@ export default {
},
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>
<style>
</style>
<style></style>

View File

@ -2,32 +2,49 @@
<div>
<div v-if="is_grouped">
<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>
<list-item-composer v-for="composer in composers.grouped[idx]"
:key="composer.id"
:composer="composer"
@click="open_composer(composer)">
<template v-slot:actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
<span
: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"
:composer="composer"
@click="open_composer(composer)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer>
</div>
</div>
<div v-else>
<list-item-composer v-for="composer in composers_list"
:key="composer.id"
:composer="composer"
@click="open_composer(composer)">
<template v-slot:actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
<list-item-composer
v-for="composer in composers_list"
:key="composer.id"
:composer="composer"
@click="open_composer(composer)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer>
</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>
</template>
@ -42,7 +59,7 @@ export default {
props: ['composers', 'media_kind'],
data () {
data() {
return {
show_details_modal: false,
selected_composer: {}
@ -51,7 +68,9 @@ export default {
computed: {
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 () {
@ -62,14 +81,17 @@ export default {
},
is_grouped: function () {
return (this.composers instanceof Composers && this.composers.options.group)
return this.composers instanceof Composers && this.composers.options.group
}
},
methods: {
open_composer: function (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) {
@ -80,5 +102,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

@ -1,10 +1,12 @@
<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">
<h1 class="title is-6">{{ composer.name }}</h1>
<h1 class="title is-6">
{{ composer.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -16,5 +18,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

@ -1,10 +1,12 @@
<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">
<h1 class="title is-6">{{ genre.name }}</h1>
<h1 class="title is-6">
{{ genre.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -16,5 +18,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,13 +1,15 @@
<template>
<div class="media">
<figure class="media-left fd-has-action" v-if="$slots.icon">
<slot name="icon"></slot>
<figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon" />
</figure>
<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 class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -19,5 +21,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,16 +1,44 @@
<template>
<div class="media" v-if="is_next || !show_only_next_items">
<div class="media-left" v-if="edit_mode">
<span class="icon has-text-grey fd-is-movable handle"><i class="mdi mdi-drag-horizontal mdi-18px"></i></span>
<div v-if="is_next || !show_only_next_items" class="media">
<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"
/></span>
</div>
<div class="media-content fd-has-action is-clipped" v-on: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>
<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 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>
<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 class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -20,14 +48,20 @@ import webapi from '@/webapi'
export default {
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: {
state () {
state() {
return this.$store.state.player
},
is_next () {
is_next() {
return this.current_position < 0 || this.position >= this.current_position
}
},
@ -40,5 +74,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,16 +1,32 @@
<template>
<div class="media" :id="'index_' + track.title_sort.charAt(0).toUpperCase()" :class="{ 'with-progress': $slots.progress }">
<figure class="media-left fd-has-action" v-if="$slots.icon">
<slot name="icon"></slot>
<div
:id="'index_' + track.title_sort.charAt(0).toUpperCase()"
class="media"
:class="{ 'with-progress': $slots.progress }"
>
<figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon" />
</figure>
<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>
<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"></slot>
<h1
class="title is-6"
:class="{
'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 class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -22,5 +38,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,18 +1,36 @@
<template>
<div>
<list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template v-slot:icon>
<list-item-playlist
v-for="playlist in playlists"
:key="playlist.id"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<template #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>
</template>
<template v-slot:actions>
<template #actions>
<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>
</template>
</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>
</template>
@ -26,7 +44,7 @@ export default {
props: ['playlists'],
data () {
data() {
return {
show_details_modal: false,
selected_playlist: {}
@ -50,5 +68,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,13 +1,24 @@
<template>
<div>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index, track)">
<template v-slot:actions>
<list-item-track
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)">
<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>
</template>
</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>
</template>
@ -22,7 +33,7 @@ export default {
props: ['tracks', 'uris', 'expression'],
data () {
data() {
return {
show_details_modal: false,
selected_track: {}
@ -48,5 +59,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,30 +1,47 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4" v-if="title">
<p v-if="title" class="title is-4">
{{ title }}
</p>
<slot name="modal-content"></slot>
<slot name="modal-content" />
</div>
<footer class="card-footer">
<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 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"></i></span> <span class="is-size-7">{{ delete_action }}</span>
<a
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 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"></i></span> <span class="is-size-7">{{ ok_action }}</span>
<a
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>
</footer>
</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>
</transition>
</div>
@ -37,5 +54,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -1,8 +1,8 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
@ -10,32 +10,54 @@
<form @submit.prevent="add_stream">
<div class="field">
<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">
<i class="mdi mdi-rss"></i>
<i class="mdi mdi-rss" />
</span>
</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>
</div>
</form>
</div>
<footer class="card-footer" v-if="loading">
<footer v-if="loading" class="card-footer">
<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>
</footer>
<footer class="card-footer" v-else>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
<footer v-else class="card-footer">
<a
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 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"></i></span> <span class="is-size-7">Add</span>
<a
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>
</footer>
</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>
</transition>
</div>
@ -48,28 +70,15 @@ export default {
name: 'ModalDialogAddRss',
props: ['show'],
data () {
data() {
return {
url: '',
loading: false
}
},
methods: {
add_stream: function () {
this.loading = true
webapi.library_add(this.url).then(() => {
this.$emit('close')
this.$emit('podcast-added')
this.url = ''
}).catch(() => {
this.loading = false
})
}
},
watch: {
'show' () {
show() {
if (this.show) {
this.loading = false
@ -79,9 +88,24 @@ export default {
}, 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>
<style>
</style>
<style></style>

View File

@ -1,44 +1,63 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
Add stream URL
</p>
<form v-on:submit.prevent="play" class="fd-has-margin-bottom">
<p class="title is-4">Add stream URL</p>
<form class="fd-has-margin-bottom" @submit.prevent="play">
<div class="field">
<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">
<i class="mdi mdi-web"></i>
<i class="mdi mdi-web" />
</span>
</p>
</div>
</form>
</div>
<footer class="card-footer" v-if="loading">
<footer v-if="loading" class="card-footer">
<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>
</footer>
<footer class="card-footer" v-else>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
<footer v-else class="card-footer">
<a
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 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 class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
<a
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>
</footer>
</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>
</transition>
</div>
@ -51,37 +70,15 @@ export default {
name: 'ModalDialogAddUrlStream',
props: ['show'],
data () {
data() {
return {
url: '',
loading: false
}
},
methods: {
add_stream: function () {
this.loading = true
webapi.queue_add(this.url).then(() => {
this.$emit('close')
this.url = ''
}).catch(() => {
this.loading = false
})
},
play: function () {
this.loading = true
webapi.player_play_uri(this.url, false).then(() => {
this.$emit('close')
this.url = ''
}).catch(() => {
this.loading = false
})
}
},
watch: {
'show' () {
show() {
if (this.show) {
this.loading = false
@ -91,9 +88,36 @@ export default {
}, 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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

@ -1,37 +1,50 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_albums">{{ composer.name }}</a>
<a class="has-text-link" @click="open_albums">{{
composer.name
}}</a>
</p>
<p>
<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>
<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>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
<span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
<span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
<span class="icon"><i class="mdi mdi-play" /></span>
<span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</div>
@ -47,31 +60,43 @@ export default {
methods: {
play: function () {
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 () {
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 () {
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 () {
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 () {
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

@ -1,13 +1,15 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
<a class="has-text-link" @click="open_playlist">{{
playlist.name
}}</a>
</p>
<div class="content is-small">
<p>
@ -20,20 +22,27 @@
</p>
</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">
<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 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 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>
</footer>
</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>
</transition>
</div>
@ -70,5 +79,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,65 +1,140 @@
<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">
<!-- Link to queue -->
<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>
<!-- 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">
<p class="is-size-7 fd-is-text-clipped">
<strong>{{ now_playing.title }}</strong><br>
{{ now_playing.artist }}<span v-if="now_playing.data_kind === 'url'"> - {{ now_playing.album }}</span>
<strong>{{ now_playing.title }}</strong
><br />
{{ now_playing.artist
}}<span v-if="now_playing.data_kind === 'url'">
- {{ now_playing.album }}</span
>
</p>
</div>
</router-link>
<!-- 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-seek-back v-if="is_now_playing_page" seek_ms="10000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-back>
<player-button-previous
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 -->
<player-button-play-pause class="navbar-item" icon_style="mdi-36px" show_disabled_message></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>
<player-button-play-pause
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) -->
<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) -->
<a 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 }"></i></span>
<a
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>
<!-- Player menu dropup menu (only visible on desktop) -->
<div class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch"
:class="{ 'is-active': show_player_menu }">
<a 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 }"></i></span>
<div
class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch"
:class="{ 'is-active': show_player_menu }"
>
<a
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>
<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">
<!-- Outputs: master volume -->
<div class="level is-mobile">
<div class="level-left fd-expanded">
<div class="level-item" style="flex-grow: 0;">
<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>
<div class="level-item" style="flex-grow: 0">
<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
}"
/></span>
</a>
</div>
<div class="level-item fd-expanded">
<div class="fd-expanded">
<p class="heading">Volume</p>
<Slider v-model="player.volume"
<Slider
v-model="player.volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
:classes="{ target: 'slider' }"
@change="set_volume"
:classes="{ target: 'slider'}" />
/>
<!--range-slider
class="slider fd-has-action"
min="0"
@ -75,28 +150,53 @@
</div>
<!-- Outputs: master volume -->
<hr class="fd-navbar-divider">
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
<hr class="fd-navbar-divider" />
<navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume -->
<hr class="fd-navbar-divider">
<hr class="fd-navbar-divider" />
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left fd-expanded">
<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>
<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" /></span
></a>
</div>
<div class="level-item 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>
<Slider v-model="stream_volume"
<p
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"
:max="100"
:step="1"
:tooltips="false"
:disabled="!playing"
:classes="{ target: 'slider' }"
@change="set_stream_volume"
:classes="{ target: 'slider'}" />
/>
<!--range-slider
class="slider fd-has-action"
min="0"
@ -113,14 +213,14 @@
</div>
<!-- Playback controls -->
<hr class="fd-navbar-divider">
<hr class="fd-navbar-divider" />
<div class="navbar-item">
<div class="level is-mobile fd-expanded">
<div class="level-item">
<div class="buttons has-addons">
<player-button-repeat class="button"></player-button-repeat>
<player-button-shuffle class="button"></player-button-shuffle>
<player-button-consume class="button"></player-button-consume>
<player-button-repeat class="button" />
<player-button-shuffle class="button" />
<player-button-consume class="button" />
</div>
</div>
</div>
@ -130,40 +230,51 @@
</div>
<!-- Player menu (only visible on mobile and tablet) -->
<div class="navbar-menu is-hidden-desktop" :class="{ 'is-active': show_player_menu }">
<div class="navbar-start">
</div>
<div
class="navbar-menu is-hidden-desktop"
:class="{ 'is-active': show_player_menu }"
>
<div class="navbar-start" />
<div class="navbar-end">
<!-- Repeat/shuffle/consume -->
<div class="navbar-item">
<div class="buttons is-centered">
<player-button-repeat class="button" icon_style="mdi-18px"></player-button-repeat>
<player-button-shuffle class="button" icon_style="mdi-18px"></player-button-shuffle>
<player-button-consume class="button" icon_style="mdi-18px"></player-button-consume>
<player-button-repeat class="button" icon_style="mdi-18px" />
<player-button-shuffle class="button" icon_style="mdi-18px" />
<player-button-consume class="button" icon_style="mdi-18px" />
</div>
</div>
<hr class="fd-navbar-divider">
<hr class="fd-navbar-divider" />
<!-- Outputs: master volume -->
<div class="navbar-item">
<div class="level is-mobile">
<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">
<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>
</div>
<div class="level-item fd-expanded">
<div class="fd-expanded">
<p class="heading">Volume</p>
<Slider v-model="player.volume"
<Slider
v-model="player.volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
:classes="{ target: 'slider' }"
@change="set_volume"
:classes="{ target: 'slider'}" />
/>
<!--range-slider
class="slider fd-has-action"
min="0"
@ -179,32 +290,54 @@
</div>
<!-- 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 -->
<hr class="fd-navbar-divider">
<hr class="fd-navbar-divider" />
<div class="navbar-item fd-has-margin-bottom">
<div class="level is-mobile">
<div class="level-left fd-expanded">
<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>
<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" />
</span>
</a>
</div>
<div class="level-item 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>
<Slider v-model="stream_volume"
<p
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"
:max="100"
:step="1"
:tooltips="false"
:disabled="!playing"
:classes="{ target: 'slider' }"
@change="set_stream_volume"
:classes="{ target: 'slider'}" />
/>
<!-- range-slider
class="slider fd-has-action"
min="0"
@ -258,7 +391,7 @@ export default {
PlayerButtonSeekBack
},
data () {
data() {
return {
old_volume: 0,
@ -273,49 +406,67 @@ export default {
computed: {
show_player_menu: {
get () {
get() {
return this.$store.state.show_player_menu
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_PLAYER_MENU, value)
}
},
show_burger_menu () {
show_burger_menu() {
return this.$store.state.show_burger_menu
},
zindex () {
zindex() {
if (this.show_burger_menu) {
return 'z-index: 20'
}
return ''
},
state () {
state() {
return this.$store.state.player
},
now_playing () {
now_playing() {
return this.$store.getters.now_playing
},
is_now_playing_page () {
is_now_playing_page() {
return this.$route.path === '/now-playing'
},
outputs () {
outputs() {
return this.$store.state.outputs
},
player () {
player() {
return this.$store.state.player
},
config () {
config() {
return this.$store.state.config
}
},
watch: {
'$store.state.player.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
// on app mounted
mounted() {
this.setupAudio()
},
// on app destroyed
unmounted() {
this.closeAudio()
},
methods: {
on_click_outside_outputs () {
on_click_outside_outputs() {
this.show_outputs_menu = false
},
@ -334,21 +485,24 @@ export default {
setupAudio: function () {
const a = _audio.setupAudio()
a.addEventListener('waiting', e => {
a.addEventListener('waiting', (e) => {
this.playing = false
this.loading = true
})
a.addEventListener('playing', e => {
a.addEventListener('playing', (e) => {
this.playing = true
this.loading = false
})
a.addEventListener('ended', e => {
a.addEventListener('ended', (e) => {
this.playing = false
this.loading = false
})
a.addEventListener('error', e => {
a.addEventListener('error', (e) => {
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.loading = false
})
@ -385,27 +539,8 @@ export default {
this.stream_volume = newVolume
_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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

@ -1,80 +1,134 @@
<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">
<navbar-item-link to="/playlists" v-if="is_visible_playlists">
<span class="icon"><i class="mdi mdi-library-music"></i></span>
<navbar-item-link v-if="is_visible_playlists" to="/playlists">
<span class="icon"><i class="mdi mdi-library-music" /></span>
</navbar-item-link>
<navbar-item-link to="/music" v-if="is_visible_music">
<span class="icon"><i class="mdi mdi-music"></i></span>
<navbar-item-link v-if="is_visible_music" to="/music">
<span class="icon"><i class="mdi mdi-music" /></span>
</navbar-item-link>
<navbar-item-link to="/podcasts" v-if="is_visible_podcasts">
<span class="icon"><i class="mdi mdi-microphone"></i></span>
<navbar-item-link v-if="is_visible_podcasts" to="/podcasts">
<span class="icon"><i class="mdi mdi-microphone" /></span>
</navbar-item-link>
<navbar-item-link to="/audiobooks" v-if="is_visible_audiobooks">
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span>
<navbar-item-link v-if="is_visible_audiobooks" to="/audiobooks">
<span class="icon"><i class="mdi mdi-book-open-variant" /></span>
</navbar-item-link>
<navbar-item-link to="/radio" v-if="is_visible_radio">
<span class="icon"><i class="mdi mdi-radio"></i></span>
<navbar-item-link v-if="is_visible_radio" to="/radio">
<span class="icon"><i class="mdi mdi-radio" /></span>
</navbar-item-link>
<navbar-item-link to="/files" v-if="is_visible_files">
<span class="icon"><i class="mdi mdi-folder-open"></i></span>
<navbar-item-link v-if="is_visible_files" to="/files">
<span class="icon"><i class="mdi mdi-folder-open" /></span>
</navbar-item-link>
<navbar-item-link to="/search" v-if="is_visible_search">
<span class="icon"><i class="mdi mdi-magnify"></i></span>
<navbar-item-link v-if="is_visible_search" to="/search">
<span class="icon"><i class="mdi mdi-magnify" /></span>
</navbar-item-link>
<div class="navbar-burger" @click="show_burger_menu = !show_burger_menu" :class="{ 'is-active': show_burger_menu }">
<span></span>
<span></span>
<span></span>
<div
class="navbar-burger"
:class="{ 'is-active': show_burger_menu }"
@click="show_burger_menu = !show_burger_menu"
>
<span />
<span />
<span />
</div>
</div>
<div class="navbar-menu" :class="{ 'is-active': show_burger_menu }">
<div class="navbar-start">
</div>
<div class="navbar-start" />
<div class="navbar-end">
<!-- Burger menu entries -->
<div class="navbar-item has-dropdown is-hoverable"
:class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings">
<div
class="navbar-item has-dropdown is-hoverable"
:class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings"
>
<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>
</a>
<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="/music" exact><span class="icon"><i class="mdi mdi-music"></i></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 to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link>
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link>
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link>
<navbar-item-link to="/radio"><span class="icon"><i class="mdi mdi-radio"></i></span> <b>Radio</b></navbar-item-link>
<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">
<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
</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 class="is-overlay" v-show="show_settings_menu"
style="z-index:10; width: 100vw; height:100vh;"
@click="show_settings_menu = false"></div>
<div
v-show="show_settings_menu"
class="is-overlay"
style="z-index: 10; width: 100vw; height: 100vh"
@click="show_settings_menu = false"
/>
</nav>
</template>
@ -86,82 +140,103 @@ export default {
name: 'NavbarTop',
components: { NavbarItemLink },
data () {
data() {
return {
show_settings_menu: false
}
},
computed: {
is_visible_playlists () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_playlists').value
is_visible_playlists() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_playlists'
).value
},
is_visible_music () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_music').value
is_visible_music() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_music'
).value
},
is_visible_podcasts () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_podcasts').value
is_visible_podcasts() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_podcasts'
).value
},
is_visible_audiobooks () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_audiobooks').value
is_visible_audiobooks() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_audiobooks'
).value
},
is_visible_radio () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_radio').value
is_visible_radio() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_radio'
).value
},
is_visible_files () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_files').value
is_visible_files() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_files'
).value
},
is_visible_search () {
return this.$store.getters.settings_option('webinterface', 'show_menu_item_search').value
is_visible_search() {
return this.$store.getters.settings_option(
'webinterface',
'show_menu_item_search'
).value
},
player () {
player() {
return this.$store.state.player
},
config () {
config() {
return this.$store.state.config
},
library () {
library() {
return this.$store.state.library
},
audiobooks () {
audiobooks() {
return this.$store.state.audiobooks_count
},
podcasts () {
podcasts() {
return this.$store.state.podcasts_count
},
spotify_enabled () {
spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid
},
show_burger_menu: {
get () {
get() {
return this.$store.state.show_burger_menu
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_BURGER_MENU, value)
}
},
show_player_menu () {
show_player_menu() {
return this.$store.state.show_player_menu
},
show_update_dialog: {
get () {
get() {
return this.$store.state.show_update_dialog
},
set (value) {
set(value) {
this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
}
},
zindex () {
zindex() {
if (this.show_player_menu) {
return 'z-index: 20'
}
@ -169,19 +244,18 @@ export default {
}
},
methods: {
on_click_outside_settings () {
this.show_settings_menu = !this.show_settings_menu
watch: {
$route(to, from) {
this.show_settings_menu = false
}
},
watch: {
$route (to, from) {
this.show_settings_menu = false
methods: {
on_click_outside_settings() {
this.show_settings_menu = !this.show_settings_menu
}
}
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,17 @@
<template>
<a @click="toggle_play_pause" :disabled="disabled">
<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>
<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
}
]"
/></span>
</a>
</template>
@ -16,16 +27,18 @@ export default {
},
computed: {
is_playing () {
is_playing() {
return this.$store.state.player.state === 'play'
},
is_pause_allowed () {
return (this.$store.getters.now_playing &&
this.$store.getters.now_playing.data_kind !== 'pipe')
is_pause_allowed() {
return (
this.$store.getters.now_playing &&
this.$store.getters.now_playing.data_kind !== 'pipe'
)
},
disabled () {
disabled() {
return !this.$store.state.queue || this.$store.state.queue.count <= 0
}
},
@ -34,7 +47,12 @@ export default {
toggle_play_pause: function () {
if (this.disabled) {
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
}
@ -51,5 +69,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,9 @@
<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>
<script>
@ -8,13 +12,13 @@ export default {
props: ['max', 'value'],
computed: {
width () {
width() {
if (this.value > 0 && this.max > 0) {
return parseInt(this.value * 100 / this.max)
return parseInt((this.value * 100) / this.max)
}
return 0
},
width_percent () {
width_percent() {
return this.width + '%'
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
<template>
<div class="media">
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist">
<h1 class="title is-6">{{ artist.name }}</h1>
<div class="media-content fd-has-action is-clipped" @click="open_artist">
<h1 class="title is-6">
{{ artist.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions"></slot>
<slot name="actions" />
</div>
</div>
</template>
@ -22,5 +24,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,18 +5,30 @@
<div class="column is-four-fifths">
<div class="tabs is-centered is-small">
<ul>
<router-link to="/audiobooks/artists" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/audiobooks/artists"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/audiobooks/albums" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/audiobooks/albums"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
@ -35,5 +47,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

@ -5,50 +5,85 @@
<div class="column is-four-fifths">
<div class="tabs is-centered is-small">
<ul>
<router-link to="/music/browse" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/music/browse"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/music/artists" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/music/artists"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/music/albums" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/music/albums"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/music/genres" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/music/genres"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/music/composers" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-slot="{ navigate, isActive }"
to="/music/composers"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
</router-link>
<router-link to="/music/spotify" v-if="spotify_enabled" custom v-slot="{ navigate, isActive }">
<li :class="{'is-active': isActive}">
<router-link
v-if="spotify_enabled"
v-slot="{ navigate, isActive }"
to="/music/spotify"
custom
>
<li :class="{ 'is-active': isActive }">
<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>
</a>
</li>
@ -66,12 +101,11 @@ export default {
name: 'TabsMusic',
computed: {
spotify_enabled () {
spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid
}
}
}
</script>
<style>
</style>
<style></style>

View File

@ -1,5 +1,5 @@
<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="columns is-centered">
<div class="column is-four-fifths">
@ -7,13 +7,17 @@
<ul>
<li :class="{ 'is-active': $route.path === '/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>
</a>
</li>
<li :class="{ 'is-active': $route.path === '/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>
</a>
</li>
@ -32,7 +36,7 @@ export default {
props: ['query'],
computed: {
spotify_enabled () {
spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid
},
@ -68,5 +72,4 @@ export default {
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,9 @@
<div class="columns is-centered">
<div class="column is-four-fifths has-text-centered-mobile">
<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>
@ -25,8 +27,14 @@
<!-- Right side -->
<div class="level-right">
<div v-if="library.updating"><a class="button is-small is-loading">Update</a></div>
<div v-else><a @click="showUpdateDialog()" class="button is-small">Update</a></div>
<div v-if="library.updating">
<a class="button is-small is-loading">Update</a>
</div>
<div v-else>
<a class="button is-small" @click="showUpdateDialog()"
>Update</a
>
</div>
</div>
</nav>
@ -34,27 +42,50 @@
<tbody>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
</tbody>
</table>
@ -68,8 +99,20 @@
<div class="columns is-centered">
<div class="column is-four-fifths">
<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">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>
<p class="is-size-7">
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>
@ -84,7 +127,7 @@ import * as types from '@/store/mutation_types'
export default {
name: 'PageAbout',
data () {
data() {
return {
show_update_dropdown: false,
show_update_library: false
@ -92,24 +135,23 @@ export default {
},
computed: {
config () {
config() {
return this.$store.state.config
},
library () {
library() {
return this.$store.state.library
}
},
methods: {
onClickOutside (event) {
onClickOutside(event) {
this.show_update_dropdown = false
},
showUpdateDialog () {
showUpdateDialog() {
this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
}
}
}
</script>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,43 +1,62 @@
<template>
<div class="fd-page-with-tabs">
<tabs-music></tabs-music>
<tabs-music />
<content-with-heading>
<template v-slot:options>
<index-button-list :index="artists_list.indexList"></index-button-list>
<template #options>
<index-button-list :index="artists_list.indexList" />
<div class="columns">
<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="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>
</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 class="field" v-if="spotify_enabled">
<div v-if="spotify_enabled" class="field">
<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>
</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 class="column">
<p class="heading" style="margin-bottom: 24px;">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu>
<p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options" />
</div>
</div>
</template>
<template v-slot:heading-left>
<template #heading-left>
<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 v-slot:heading-right>
</template>
<template v-slot:content>
<list-artists :artists="artists_list"></list-artists>
<template #heading-right />
<template #content>
<list-artists :artists="artists_list" />
</template>
</content-with-heading>
</div>
@ -65,9 +84,32 @@ const dataObject = {
export default {
name: 'PageArtists',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu },
components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListArtists,
DropdownMenu
},
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (this.artists.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
artists: { items: [] },
sort_options: ['Name', 'Recently added']
@ -75,7 +117,7 @@ export default {
},
computed: {
artists_list () {
artists_list() {
return new Artists(this.artists.items, {
hideSingles: this.hide_singles,
hideSpotify: this.hide_spotify,
@ -84,33 +126,33 @@ export default {
})
},
spotify_enabled () {
spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid
},
hide_singles: {
get () {
get() {
return this.$store.state.hide_singles
},
set (value) {
set(value) {
this.$store.commit(types.HIDE_SINGLES, value)
}
},
hide_spotify: {
get () {
get() {
return this.$store.state.hide_spotify
},
set (value) {
set(value) {
this.$store.commit(types.HIDE_SPOTIFY, value)
}
},
sort: {
get () {
get() {
return this.$store.state.artists_sort
},
set (value) {
set(value) {
this.$store.commit(types.ARTISTS_SORT, value)
}
}
@ -120,26 +162,8 @@ export default {
scrollToTop: function () {
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

@ -1,22 +1,36 @@
<template>
<content-with-heading>
<template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p>
<template #heading-left>
<p class="title is-4">
{{ artist.name }}
</p>
</template>
<template v-slot:heading-right>
<template #heading-right>
<div class="buttons is-centered">
<a 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"></i></span>
<a
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 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>
</div>
</template>
<template v-slot:content>
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums</p>
<list-albums :albums="albums.items"></list-albums>
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
<template #content>
<p class="heading has-text-centered-mobile">
{{ artist.album_count }} albums
</p>
<list-albums :albums="albums.items" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@close="show_artist_details_modal = false"
/>
</template>
</content-with-heading>
</template>
@ -45,7 +59,20 @@ export default {
name: 'PageAudiobooksArtist',
components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
artist: {},
albums: {},
@ -56,24 +83,13 @@ export default {
methods: {
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,59 @@
<template>
<div>
<content-with-heading>
<template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
<template #options>
<index-button-list :index="index_list" />
</template>
<template v-slot:heading-left>
<p class="title is-4">{{ name }}</p>
<template #heading-left>
<p class="title is-4">
{{ name }}
</p>
</template>
<template v-slot:heading-right>
<template #heading-right>
<div class="buttons is-centered">
<a 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"></i></span>
<a
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 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>
</div>
</template>
<template v-slot:content>
<p class="heading has-text-centered-mobile">{{ 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 #content>
<p class="heading has-text-centered-mobile">
{{ 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>
<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>
</template>
</list-item-albums>
<modal-dialog-album :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" />
<modal-dialog-album
: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>
</content-with-heading>
</div>
@ -53,9 +79,27 @@ const dataObject = {
export default {
name: 'PageComposer',
components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer },
components: {
ContentWithHeading,
ListItemAlbums,
ModalDialogAlbum,
ModalDialogComposer
},
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
name: '',
composer_albums: { items: [] },
@ -67,20 +111,31 @@ export default {
},
computed: {
index_list () {
return [...new Set(this.composer_albums.items
.map(album => album.name_sort.charAt(0).toUpperCase()))]
index_list() {
return [
...new Set(
this.composer_albums.items.map((album) =>
album.name_sort.charAt(0).toUpperCase()
)
)
]
}
},
methods: {
open_tracks: function () {
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 () {
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) {
@ -91,22 +146,8 @@ export default {
this.selected_album = album
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>
<style>
</style>
<style></style>

View File

@ -1,33 +1,59 @@
<template>
<div>
<content-with-heading>
<template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
<template #options>
<index-button-list :index="index_list" />
</template>
<template v-slot:heading-left>
<p class="title is-4">{{ composer }}</p>
<template #heading-left>
<p class="title is-4">
{{ composer }}
</p>
</template>
<template v-slot:heading-right>
<template #heading-right>
<div class="buttons is-centered">
<a 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"></i></span>
<a
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 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>
</div>
</template>
<template v-slot:content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_albums">albums</a> | {{ tracks.total }} tracks</p>
<list-item-track v-for="(track, index) in rated_tracks" :key="track.id" :track="track" @click="play_track(index)">
<template v-slot:actions>
<template #content>
<p class="heading has-text-centered-mobile">
<a class="has-text-link" @click="open_albums">albums</a> |
{{ 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)">
<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>
</template>
</list-item-track>
<modal-dialog-track :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" />
<modal-dialog-track
: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>
</content-with-heading>
</div>
@ -53,9 +79,27 @@ const dataObject = {
export default {
name: 'PageComposerTracks',
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogComposer },
components: {
ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ModalDialogComposer
},
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
tracks: { items: [] },
composer: '',
@ -70,28 +114,45 @@ export default {
},
computed: {
index_list () {
return [...new Set(this.tracks.items
.map(track => track.title_sort.charAt(0).toUpperCase()))]
index_list() {
return [
...new Set(
this.tracks.items.map((track) =>
track.title_sort.charAt(0).toUpperCase()
)
)
]
},
rated_tracks () {
return this.tracks.items.filter(track => track.rating >= this.min_rating)
rated_tracks() {
return this.tracks.items.filter(
(track) => track.rating >= this.min_rating
)
}
},
methods: {
open_albums: function () {
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 () {
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) {
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) {
@ -105,22 +166,8 @@ export default {
this.selected_track = track
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,35 @@
<template>
<div class="fd-page-with-tabs">
<tabs-music></tabs-music>
<tabs-music />
<content-with-heading>
<template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
<template #options>
<index-button-list :index="index_list" />
</template>
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Genres</p>
<p class="heading">{{ genres.total }} genres</p>
</template>
<template v-slot:content>
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre" @click="open_genre(genre)">
<template v-slot:actions>
<template #content>
<list-item-genre
v-for="genre in genres.items"
:key="genre.name"
:genre="genre"
@click="open_genre(genre)"
>
<template #actions>
<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>
</template>
</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>
</content-with-heading>
</div>
@ -44,9 +55,28 @@ const dataObject = {
export default {
name: 'PageGenres',
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre },
components: {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListItemGenre,
ModalDialogGenre
},
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
genres: { items: [] },
@ -56,9 +86,12 @@ export default {
},
computed: {
index_list () {
return [...new Set(this.genres.items
.map(genre => genre.name.charAt(0).toUpperCase()))]
index_list() {
return [
...new Set(
this.genres.items.map((genre) => genre.name.charAt(0).toUpperCase())
)
]
}
},
@ -71,22 +104,8 @@ export default {
this.selected_genre = genre
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

@ -1,31 +1,46 @@
<template>
<content-with-heading>
<template v-slot:heading-left>
<div class="title is-4">{{ album.name }}
<template #heading-left>
<div class="title is-4">
{{ album.name }}
</div>
</template>
<template v-slot:heading-right>
</template>
<template #heading-right>
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
<a
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 class="button is-small is-dark is-rounded" @click="play">
<span class="icon">
<i class="mdi mdi-play"></i>
<i class="mdi mdi-play" />
</span>
<span>Play</span>
</a>
</div>
</template>
<template v-slot:content>
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<list-item-track v-for="track in tracks" :key="track.id" :track="track" @click="play_track(track)">
<template v-slot:progress>
<template #content>
<p class="heading has-text-centered-mobile">
{{ album.track_count }} tracks
</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" />
</template>
<template v-slot:actions>
<template #actions>
<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>
</template>
</list-item-track>
@ -33,7 +48,8 @@
:show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
@play-count-changed="reload_tracks" />
@play-count-changed="reload_tracks"
/>
<modal-dialog-album
:show="show_album_details_modal"
:album="album"
@ -41,16 +57,22 @@
:new_tracks="new_tracks"
@close="show_album_details_modal = false"
@play-count-changed="reload_tracks"
@remove-podcast="open_remove_podcast_dialog" />
@remove-podcast="open_remove_podcast_dialog"
/>
<modal-dialog
:show="show_remove_podcast_modal"
title="Remove podcast"
delete_action="Remove"
@close="show_remove_podcast_modal = false"
@delete="remove_podcast">
<template v-slot:modal-content>
@delete="remove_podcast"
>
<template #modal-content>
<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>
</modal-dialog>
</template>
@ -91,7 +113,20 @@ export default {
ProgressBar
},
data () {
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
},
data() {
return {
album: {},
tracks: [],
@ -107,8 +142,8 @@ export default {
},
computed: {
new_tracks () {
return this.tracks.filter(track => track.play_count === 0).length
new_tracks() {
return this.tracks.filter((track) => track.play_count === 0).length
}
},
@ -129,9 +164,12 @@ export default {
open_remove_podcast_dialog: function () {
this.show_album_details_modal = false
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) {
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
}
@ -142,9 +180,11 @@ export default {
remove_podcast: function () {
this.show_remove_podcast_modal = false
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => {
this.$router.replace({ path: '/podcasts' })
})
webapi
.library_playlist_delete(this.rss_playlist_to_remove.id)
.then(() => {
this.$router.replace({ path: '/podcasts' })
})
},
reload_tracks: function () {
@ -152,22 +192,8 @@ export default {
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>
<style>
</style>
<style></style>

View File

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

View File

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

View File

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

View File

@ -5,91 +5,122 @@
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form v-on:submit.prevent="new_search">
<form @submit.prevent="new_search">
<div class="field">
<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">
<i class="mdi mdi-magnify"></i>
<i class="mdi mdi-magnify" />
</span>
</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
with <code>query:</code>.
<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 with <code>query:</code>.
</p>
</div>
</form>
<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>
<div class="tags" style="margin-top: 16px">
<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>
</section>
<tabs-search :query="search_query"></tabs-search>
<tabs-search :query="search_query" />
<!-- Tracks -->
<content-with-heading v-if="show_tracks && tracks.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Tracks</p>
</template>
<template v-slot:content>
<list-tracks :tracks="tracks.items"></list-tracks>
<template #content>
<list-tracks :tracks="tracks.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_tracks_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_tracks && !tracks.total" class="mt-6">
<template v-slot:content>
<template #content>
<p><i>No tracks found</i></p>
</template>
</content-text>
<!-- Artists -->
<content-with-heading v-if="show_artists && artists.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Artists</p>
</template>
<template v-slot:content>
<list-artists :artists="artists.items"></list-artists>
<template #content>
<list-artists :artists="artists.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_artists_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_artists && !artists.total">
<template v-slot:content>
<template #content>
<p><i>No artists found</i></p>
</template>
</content-text>
<!-- Albums -->
<content-with-heading v-if="show_albums && albums.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Albums</p>
</template>
<template v-slot:content>
<list-albums :albums="albums.items"></list-albums>
<template #content>
<list-albums :albums="albums.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_albums_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_albums && !albums.total">
<template v-slot:content>
<template #content>
<p><i>No albums found</i></p>
</template>
</content-text>
@ -100,12 +131,16 @@
<p class="title is-4">Composers</p>
</template>
<template slot:content>
<list-composers :composers="composers.items"></list-composers>
<list-composers :composers="composers.items" />
</template>
<template slot:footer>
<nav v-if="show_all_composers_button" class="level">
<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>
</nav>
</template>
@ -118,66 +153,78 @@
<!-- Playlists -->
<content-with-heading v-if="show_playlists && playlists.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Playlists</p>
</template>
<template v-slot:content>
<list-playlists :playlists="playlists.items"></list-playlists>
<template #content>
<list-playlists :playlists="playlists.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_playlists_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_playlists && !playlists.total">
<template v-slot:content>
<template #content>
<p><i>No playlists found</i></p>
</template>
</content-text>
<!-- Podcasts -->
<content-with-heading v-if="show_podcasts && podcasts.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Podcasts</p>
</template>
<template v-slot:content>
<list-albums :albums="podcasts.items"></list-albums>
<template #content>
<list-albums :albums="podcasts.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_podcasts_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_podcasts && !podcasts.total">
<template v-slot:content>
<template #content>
<p><i>No podcasts found</i></p>
</template>
</content-text>
<!-- Audiobooks -->
<content-with-heading v-if="show_audiobooks && audiobooks.total">
<template v-slot:heading-left>
<template #heading-left>
<p class="title is-4">Audiobooks</p>
</template>
<template v-slot:content>
<list-albums :albums="audiobooks.items"></list-albums>
<template #content>
<list-albums :albums="audiobooks.items" />
</template>
<template v-slot:footer>
<template #footer>
<nav v-if="show_all_audiobooks_button" class="level">
<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>
</nav>
</template>
</content-with-heading>
<content-text v-if="show_audiobooks && !audiobooks.total">
<template v-slot:content>
<template #content>
<p><i>No audiobooks found</i></p>
</template>
</content-text>
@ -198,9 +245,18 @@ import * as types from '@/store/mutation_types'
export default {
name: 'PageSearch',
components: { ContentWithHeading, ContentText, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists, ListComposers },
components: {
ContentWithHeading,
ContentText,
TabsSearch,
ListTracks,
ListArtists,
ListAlbums,
ListPlaylists,
ListComposers
},
data () {
data() {
return {
search_query: '',
@ -215,64 +271,85 @@ export default {
},
computed: {
recent_searches () {
recent_searches() {
return this.$store.state.recent_searches
},
show_tracks () {
show_tracks() {
return this.$route.query.type && this.$route.query.type.includes('track')
},
show_all_tracks_button () {
show_all_tracks_button() {
return this.tracks.total > this.tracks.items.length
},
show_artists () {
show_artists() {
return this.$route.query.type && this.$route.query.type.includes('artist')
},
show_all_artists_button () {
show_all_artists_button() {
return this.artists.total > this.artists.items.length
},
show_albums () {
show_albums() {
return this.$route.query.type && this.$route.query.type.includes('album')
},
show_all_albums_button () {
show_all_albums_button() {
return this.albums.total > this.albums.items.length
},
show_composers () {
return this.$route.query.type && this.$route.query.type.includes('composer')
show_composers() {
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
},
show_playlists () {
return this.$route.query.type && this.$route.query.type.includes('playlist')
show_playlists() {
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
},
show_audiobooks () {
return this.$route.query.type && this.$route.query.type.includes('audiobook')
show_audiobooks() {
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
},
show_podcasts () {
return this.$route.query.type && this.$route.query.type.includes('podcast')
show_podcasts() {
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
},
is_visible_artwork () {
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
is_visible_artwork() {
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: {
search: function (route) {
if (!route.query.query || route.query.query === '') {
@ -289,7 +366,12 @@ export default {
},
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
}
@ -313,8 +395,12 @@ export default {
this.tracks = data.tracks ? data.tracks : { items: [], total: 0 }
this.artists = data.artists ? data.artists : { items: [], total: 0 }
this.albums = data.albums ? data.albums : { items: [], total: 0 }
this.composers = data.composers ? data.composers : { items: [], total: 0 }
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 }
this.composers = data.composers
? 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:')) {
searchParams.expression = query.query.replace(/^query:/, '').trim()
} 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) {
@ -357,7 +448,12 @@ export default {
if (query.query.startsWith('query:')) {
searchParams.expression = query.query.replace(/^query:/, '').trim()
} 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) {
@ -458,7 +554,10 @@ export default {
},
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) {
@ -494,19 +593,8 @@ export default {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
}
},
mounted: function () {
this.search(this.$route)
},
watch: {
'$route' (to, from) {
this.search(to)
}
}
}
</script>
<style>
</style>
<style></style>

View File

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

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