mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-24 05:03:17 -05:00
Merge pull request #917 from chme/web_next
Update player web interface (v0.7.0)
This commit is contained in:
commit
d300ed2a40
@ -100,7 +100,11 @@ dist_htdocsplayerjs_DATA = \
|
||||
player/js/app.js \
|
||||
player/js/app.js.map \
|
||||
player/js/chunk-vendors.js \
|
||||
player/js/chunk-vendors.js.map
|
||||
player/js/chunk-vendors.js.map \
|
||||
player/js/app-legacy.js \
|
||||
player/js/app-legacy.js.map \
|
||||
player/js/chunk-vendors-legacy.js \
|
||||
player/js/chunk-vendors-legacy.js.map
|
||||
|
||||
htdocsplayerimgdir = $(datadir)/forked-daapd/htdocs/player/img
|
||||
|
||||
|
@ -1 +1 @@
|
||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web 2</title><link rel=apple-touch-icon sizes=120x120 href=/apple-touch-icon.png?ver1.1><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 href=/player/css/app.css rel=preload as=style><link href=/player/css/chunk-vendors.css rel=preload as=style><link href=/player/js/app.js rel=preload as=script><link href=/player/js/chunk-vendors.js rel=preload as=script><link href=/player/css/chunk-vendors.css rel=stylesheet><link href=/player/css/app.css rel=stylesheet></head><body><div id=app></div><script src=/player/js/chunk-vendors.js></script><script src=/player/js/app.js></script></body></html>
|
||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web 2</title><link rel=apple-touch-icon sizes=120x120 href=/apple-touch-icon.png?ver1.1><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 href=/player/css/app.css rel=preload as=style><link href=/player/css/chunk-vendors.css rel=preload as=style><link href=/player/js/app.js rel=modulepreload as=script><link href=/player/js/chunk-vendors.js rel=modulepreload as=script><link href=/player/css/chunk-vendors.css rel=stylesheet><link href=/player/css/app.css rel=stylesheet></head><body><div id=app></div><script type=module src=/player/js/chunk-vendors.js></script><script type=module src=/player/js/app.js></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src=/player/js/chunk-vendors-legacy.js nomodule></script><script src=/player/js/app-legacy.js nomodule></script></body></html>
|
@ -1,2 +1,2 @@
|
||||
.fd-notifications{position:fixed;bottom:60px;z-index:20000;width:100%}.fd-notifications .notification{margin-bottom:10px;margin-left:24px;margin-right:24px;box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.slider{min-width:250px;width:100%}.range-slider-fill{background-color:#363636}.track-progress{margin:0;padding:0;min-width:250px;width:100%}.track-progress .range-slider-knob{visibility:hidden}.track-progress .range-slider-fill{background-color:#3273dc;height:2px}.track-progress .range-slider-rail{background-color:#fff}.media.with-progress h2:last-of-type{margin-bottom:6px}.media.with-progress{margin-top:0}a.navbar-item{outline:0;line-height:1.5;padding:.5rem 1rem}.fd-expanded{-webkit-box-flex:1;flex-grow:1;flex-shrink:1}.fd-margin-left-auto{margin-left:auto}.fd-has-action{cursor:pointer}.fd-is-movable{cursor:move}.fd-has-margin-top{margin-top:24px}.fd-has-margin-bottom{margin-bottom:24px}.fd-remove-padding-bottom{padding-bottom:0}.fd-has-padding-left-right{padding-left:24px;padding-right:24px}.fd-is-square .button{height:27px;width:27px}.fd-is-text-clipped{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fd-is-fullheight{height:calc(100vh - 6.5rem)}.fd-is-fullheight-body{flex-shrink:1;overflow:hidden;height:100%}.fd-image-fullheight{height:100%;width:auto}.fd-tabs-section{padding-bottom:3px;padding-top:3px;background:#fff;top:3.25rem;z-index:20;position:fixed;width:100%}section.fd-tabs-section+section.fd-content{margin-top:24px}.fd-progress-bar{top:52px!important}.fd-has-shadow{box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.sortable-chosen .media-right{visibility:hidden}.sortable-ghost h1,.sortable-ghost h2{color:#ff3860!important}.media:first-of-type{padding-top:17px;margin-top:16px}.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .4s;transition:opacity .4s}.fade-enter,.fade-leave-to{opacity:0}.seek-slider{min-width:250px;max-width:500px;width:100%!important}.seek-slider .range-slider-fill{background-color:#00d1b2;box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.seek-slider .range-slider-knob{width:10px;height:10px;background-color:#00d1b2;border-color:#00d1b2}.title:not(.is-spaced)+.subtitle,.title:not(.is-spaced)+.subtitle+.subtitle{margin-top:-1.3rem!important}.fd-modal-card{overflow:visible}.fd-modal-card .card-content{max-height:calc(100vh - 200px);overflow:auto}.fd-modal-card .card{margin-left:16px;margin-right:16px}
|
||||
.fd-notifications{position:fixed;bottom:60px;z-index:20000;width:100%}.fd-notifications .notification{margin-bottom:10px;margin-left:24px;margin-right:24px;box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.slider{min-width:250px;width:100%}.range-slider-fill{background-color:#363636}.track-progress{margin:0;padding:0;min-width:250px;width:100%}.track-progress .range-slider-knob{visibility:hidden}.track-progress .range-slider-fill{background-color:#3273dc;height:2px}.track-progress .range-slider-rail{background-color:#fff}.media.with-progress h2:last-of-type{margin-bottom:6px}.media.with-progress{margin-top:0}a.navbar-item{outline:0;line-height:1.5;padding:.5rem 1rem}.fd-expanded{flex-grow:1;flex-shrink:1}.fd-margin-left-auto{margin-left:auto}.fd-has-action{cursor:pointer}.fd-is-movable{cursor:move}.fd-has-margin-top{margin-top:24px}.fd-has-margin-bottom{margin-bottom:24px}.fd-remove-padding-bottom{padding-bottom:0}.fd-has-padding-left-right{padding-left:24px;padding-right:24px}.fd-is-square .button{height:27px;width:27px}.fd-is-text-clipped{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fd-tabs-section{padding-bottom:3px;padding-top:3px;background:#fff;top:3.25rem;z-index:20;position:fixed;width:100%}section.fd-tabs-section+section.fd-content{margin-top:24px}.fd-progress-bar{top:52px!important}.fd-has-shadow{box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.fd-is-fullheight{height:calc(100vh - 6.5rem);display:flex;flex-direction:column;justify-content:center}.fd-is-fullheight .fd-is-expanded{max-height:calc(100vh - 25rem);padding:1.5rem;overflow:hidden;flex-grow:1;flex-shrink:1}.fd-cover-image{height:100%}.fd-cover-image img{width:100%;height:100%;object-fit:contain;object-position:bottom;-webkit-filter:drop-shadow(0 0 1px rgba(0,0,0,.3)) drop-shadow(0 0 10px rgba(0,0,0,.3));filter:drop-shadow(0 0 1px rgba(0,0,0,.3)) drop-shadow(0 0 10px rgba(0,0,0,.3))}.sortable-chosen .media-right{visibility:hidden}.sortable-ghost h1,.sortable-ghost h2{color:#ff3860!important}.media:first-of-type{padding-top:17px;margin-top:16px}.fade-enter-active,.fade-leave-active{transition:opacity .4s}.fade-enter,.fade-leave-to{opacity:0}.seek-slider{min-width:250px;max-width:500px;width:100%!important}.seek-slider .range-slider-fill{background-color:#00d1b2;box-shadow:0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.seek-slider .range-slider-knob{width:10px;height:10px;background-color:#00d1b2;border-color:#00d1b2}.title:not(.is-spaced)+.subtitle,.title:not(.is-spaced)+.subtitle+.subtitle{margin-top:-1.3rem!important}.fd-modal-card{overflow:visible}.fd-modal-card .card-content{max-height:calc(100vh - 200px);overflow:auto}.fd-modal-card .card{margin-left:16px;margin-right:16px}.dropdown-item a{display:block}.dropdown-item:hover{background-color:#f5f5f5}
|
||||
/*# sourceMappingURL=app.css.map */
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
htdocs/player/js/app-legacy.js
Normal file
2
htdocs/player/js/app-legacy.js
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/player/js/app-legacy.js.map
Normal file
1
htdocs/player/js/app-legacy.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
51
htdocs/player/js/chunk-vendors-legacy.js
Normal file
51
htdocs/player/js/chunk-vendors-legacy.js
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/player/js/chunk-vendors-legacy.js.map
Normal file
1
htdocs/player/js/chunk-vendors-legacy.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ module.exports = {
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
|
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
|
8337
web-src/package-lock.json
generated
8337
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,48 @@
|
||||
{
|
||||
"name": "forked-daapd-web",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"description": "forked-daapd web interface",
|
||||
"author": "chme <christian.meffert@googlemail.com>",
|
||||
"license": "GPL-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --no-clean",
|
||||
"lint": "vue-cli-service lint"
|
||||
"build": "vue-cli-service build --no-clean --modern",
|
||||
"lint": "vue-cli-service lint",
|
||||
"dev": "vue-cli-service serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.1",
|
||||
"bulma": "^0.8.0",
|
||||
"axios": "^0.19.2",
|
||||
"bulma": "^0.8.2",
|
||||
"core-js": "^3.6.5",
|
||||
"mdi": "^2.2.43",
|
||||
"moment": "^2.24.0",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"npm": "^6.13.6",
|
||||
"npm": "^6.14.4",
|
||||
"reconnectingwebsocket": "^1.0.0",
|
||||
"spotify-web-api-js": "^1.2.0",
|
||||
"v-click-outside": "^3.0.0",
|
||||
"string-to-color": "^2.1.3",
|
||||
"v-click-outside": "^3.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-progressbar": "^0.7.5",
|
||||
"vue-range-slider": "^0.6.0",
|
||||
"vue-router": "^3.1.3",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "^3.1.2"
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-eslint": "^3.12.1",
|
||||
"@vue/cli-service": "^3.12.1",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
},
|
||||
"license": "GPL-2.0"
|
||||
}
|
||||
|
@ -6,8 +6,12 @@
|
||||
<!-- Setting v-show to true on the router-view tag avoids jumpiness during transitions -->
|
||||
<router-view v-show="true" />
|
||||
</transition>
|
||||
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" />
|
||||
<notifications v-show="!show_burger_menu" />
|
||||
<navbar-bottom v-show="!show_burger_menu" />
|
||||
<navbar-bottom />
|
||||
<div class="is-overlay" v-show="show_burger_menu || show_player_menu"
|
||||
style="z-index:25; width: 100vw; height:100vh;background-color: rgba(10, 10, 10, 0.2);"
|
||||
@click="show_burger_menu = show_player_menu = false"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -15,25 +19,40 @@
|
||||
import NavbarTop from '@/components/NavbarTop'
|
||||
import NavbarBottom from '@/components/NavbarBottom'
|
||||
import Notifications from '@/components/Notifications'
|
||||
import ModalDialogRemotePairing from '@/components/ModalDialogRemotePairing'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import ReconnectingWebSocket from 'reconnectingwebsocket'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { NavbarTop, NavbarBottom, Notifications },
|
||||
components: { NavbarTop, NavbarBottom, Notifications, ModalDialogRemotePairing },
|
||||
template: '<App/>',
|
||||
|
||||
data () {
|
||||
return {
|
||||
token_timer_id: 0,
|
||||
reconnect_attempts: 0
|
||||
reconnect_attempts: 0,
|
||||
pairing_active: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
show_burger_menu () {
|
||||
return this.$store.state.show_burger_menu
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
},
|
||||
show_player_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -47,7 +66,7 @@ export default {
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
if (to.meta.show_progress) {
|
||||
if (to.meta.progress !== undefined) {
|
||||
let meta = to.meta.progress
|
||||
const meta = to.meta.progress
|
||||
this.$Progress.parseMeta(meta)
|
||||
}
|
||||
this.$Progress.start()
|
||||
@ -204,17 +223,25 @@ export default {
|
||||
update_pairing: function () {
|
||||
webapi.pairing().then(({ data }) => {
|
||||
this.$store.commit(types.UPDATE_PAIRING, data)
|
||||
this.pairing_active = data.active
|
||||
})
|
||||
},
|
||||
|
||||
update_is_clipped: function () {
|
||||
if (this.show_burger_menu || this.show_player_menu) {
|
||||
document.querySelector('html').classList.add('is-clipped')
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('is-clipped')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show_burger_menu' () {
|
||||
if (this.show_burger_menu) {
|
||||
document.querySelector('html').classList.add('is-clipped')
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('is-clipped')
|
||||
}
|
||||
this.update_is_clipped()
|
||||
},
|
||||
'show_player_menu' () {
|
||||
this.update_is_clipped()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
web-src/src/components/CoverArtwork.vue
Normal file
110
web-src/src/components/CoverArtwork.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<figure>
|
||||
<img
|
||||
v-show="artwork_visible"
|
||||
:src="artwork_url_with_size"
|
||||
@load="artwork_loaded"
|
||||
@error="artwork_error"
|
||||
@click="$emit('click')">
|
||||
<img
|
||||
v-show="!artwork_visible"
|
||||
:src="dataURI"
|
||||
:alt="alt_text"
|
||||
@click="$emit('click')">
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import SVGRenderer from '@/lib/SVGRenderer'
|
||||
import stringToColor from 'string-to-color'
|
||||
|
||||
export default {
|
||||
name: 'CoverArtwork',
|
||||
props: ['artist', 'album', 'artwork_url'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
svg: new SVGRenderer(),
|
||||
width: 600,
|
||||
height: 600,
|
||||
font_family: 'sans-serif',
|
||||
font_size: 200,
|
||||
font_weight: 600,
|
||||
|
||||
artwork_visible: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
artwork_url_with_size: function () {
|
||||
return webapi.artwork_url_append_size_params(this.artwork_url)
|
||||
},
|
||||
|
||||
alt_text () {
|
||||
return this.artist + ' - ' + this.album
|
||||
},
|
||||
|
||||
caption () {
|
||||
if (this.album) {
|
||||
return this.album.substring(0, 2)
|
||||
}
|
||||
if (this.artist) {
|
||||
return this.artist.substring(0, 2)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
background_color () {
|
||||
return stringToColor(this.alt_text)
|
||||
},
|
||||
|
||||
is_background_light () {
|
||||
// Based on https://stackoverflow.com/a/44615197
|
||||
const hex = this.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
|
||||
|
||||
return luma > 0.5
|
||||
},
|
||||
|
||||
text_color () {
|
||||
return this.is_background_light ? '#000000' : '#ffffff'
|
||||
},
|
||||
|
||||
rendererParams () {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
textColor: this.text_color,
|
||||
backgroundColor: this.background_color,
|
||||
caption: this.caption,
|
||||
fontFamily: this.font_family,
|
||||
fontSize: this.font_size,
|
||||
fontWeight: this.font_weight
|
||||
}
|
||||
},
|
||||
|
||||
dataURI () {
|
||||
return this.svg.render(this.rendererParams)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
artwork_loaded: function () {
|
||||
this.artwork_visible = true
|
||||
},
|
||||
|
||||
artwork_error: function () {
|
||||
this.artwork_visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -18,7 +18,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemDirectory',
|
||||
props: [ 'directory' ]
|
||||
props: ['directory']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemGenre',
|
||||
props: [ 'genre' ]
|
||||
props: ['genre']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -34,7 +34,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.player_play({ 'item_id': this.item.id })
|
||||
webapi.player_play({ item_id: this.item.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,25 @@
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div class="modal-content fd-modal-card">
|
||||
<slot name="modal-content"></slot>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4" v-if="title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<slot name="modal-content"></slot>
|
||||
</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">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>
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
</div>
|
||||
@ -15,7 +33,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ModalDialog',
|
||||
props: [ 'show' ]
|
||||
props: ['show', 'title', 'ok_action', 'delete_action']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
87
web-src/src/components/ModalDialogAddRss.vue
Normal file
87
web-src/src/components/ModalDialogAddRss.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">Add Podcast RSS feed URL</p>
|
||||
<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">
|
||||
<span class="icon is-left">
|
||||
<i class="mdi mdi-rss"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="help">Adding a podcast includes creating an RSS playlist, that will allow forked-daapd to manage the podcast subscription.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="loading">
|
||||
<a class="card-footer-item button is-loading">
|
||||
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Processing ...</span>
|
||||
</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>
|
||||
</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>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogAddRss',
|
||||
props: ['show'],
|
||||
|
||||
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' () {
|
||||
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.url_field.focus()
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -49,7 +49,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogAddUrlStream',
|
||||
props: [ 'show' ],
|
||||
props: ['show'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
|
@ -6,12 +6,18 @@
|
||||
<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>
|
||||
<cover-artwork
|
||||
:artwork_url="album.artwork_url"
|
||||
:artist="album.artist"
|
||||
:album="album.name"
|
||||
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>
|
||||
</p>
|
||||
<div class="buttons" v-if="media_kind === '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>
|
||||
<div class="content is-small">
|
||||
<p v-if="album.artist && media_kind !== 'audiobook'">
|
||||
<span class="heading">Album artist</span>
|
||||
@ -47,11 +53,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogAlbum',
|
||||
props: [ 'show', 'album', 'media_kind' ],
|
||||
components: { CoverArtwork },
|
||||
props: ['show', 'album', 'media_kind', 'new_tracks'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -95,6 +103,13 @@ export default {
|
||||
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')
|
||||
})
|
||||
},
|
||||
|
||||
artwork_loaded: function () {
|
||||
this.artwork_visible = true
|
||||
},
|
||||
|
@ -44,7 +44,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogArtist',
|
||||
props: [ 'show', 'artist' ],
|
||||
props: ['show', 'artist'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -34,7 +34,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogDirectory',
|
||||
props: [ 'show', 'directory' ],
|
||||
props: ['show', 'directory'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -34,7 +34,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogGenre',
|
||||
props: [ 'show', 'genre' ],
|
||||
props: ['show', 'genre'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -44,7 +44,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogPlaylist',
|
||||
props: [ 'show', 'playlist' ],
|
||||
props: ['show', 'playlist'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -46,7 +46,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogPlaylistSave',
|
||||
props: [ 'show' ],
|
||||
props: ['show'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
|
@ -79,7 +79,7 @@ import SpotifyWebApi from 'spotify-web-api-js'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogQueueItem',
|
||||
props: [ 'show', 'item' ],
|
||||
props: ['show', 'item'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -95,7 +95,7 @@ export default {
|
||||
|
||||
play: function () {
|
||||
this.$emit('close')
|
||||
webapi.player_play({ 'item_id': this.item.id })
|
||||
webapi.player_play({ item_id: this.item.id })
|
||||
},
|
||||
|
||||
open_album: function () {
|
||||
|
82
web-src/src/components/ModalDialogRemotePairing.vue
Normal file
82
web-src/src/components/ModalDialogRemotePairing.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
Remote pairing request
|
||||
</p>
|
||||
<form v-on: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">
|
||||
</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>
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogRemotePairing',
|
||||
props: ['show'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
pairing_req: { pin: '' }
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pairing () {
|
||||
return this.$store.state.pairing
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
kickoff_pairing () {
|
||||
webapi.pairing_kickoff(this.pairing_req).then(() => {
|
||||
this.pairing_req.pin = ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
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.pin_field.focus()
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -152,14 +152,14 @@ export default {
|
||||
},
|
||||
|
||||
mark_new: function () {
|
||||
webapi.library_track_update(this.track.id, { 'play_count': 'reset' }).then(() => {
|
||||
webapi.library_track_update(this.track.id, { play_count: 'reset' }).then(() => {
|
||||
this.$emit('play_count_changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
},
|
||||
|
||||
mark_played: function () {
|
||||
webapi.library_track_update(this.track.id, { 'play_count': 'increment' }).then(() => {
|
||||
webapi.library_track_update(this.track.id, { play_count: 'increment' }).then(() => {
|
||||
this.$emit('play_count_changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
|
@ -1,40 +1,377 @@
|
||||
<template>
|
||||
<nav class="navbar is-dark is-fixed-bottom" role="navigation" aria-label="player controls">
|
||||
<nav class="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">
|
||||
<router-link to="/" class="navbar-item" active-class="is-active" exact>
|
||||
|
||||
<!-- Link to queue -->
|
||||
<navbar-item-link to="/" exact>
|
||||
<span class="icon"><i class="mdi mdi-24px mdi-playlist-play"></i></span>
|
||||
</router-link>
|
||||
<router-link to="/now-playing" class="navbar-item is-expanded is-clipped" active-class="is-active" exact>
|
||||
<div>
|
||||
</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>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
<player-button-play-pause class="navbar-item fd-margin-left-auto" icon_style="mdi-36px" show_disabled_message></player-button-play-pause>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
<!-- 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 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>
|
||||
|
||||
<!-- 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>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading">Volume</p>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:value="player.volume"
|
||||
@change="set_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs: master volume -->
|
||||
<hr class="navbar-divider">
|
||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||
|
||||
<!-- Outputs: stream volume -->
|
||||
<hr class="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>
|
||||
<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>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playback controls -->
|
||||
<hr class="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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player menu (only visible on mobile and tablet) -->
|
||||
<div class="navbar-menu is-hidden-desktop" style="max-height: calc(100vh - 3.25rem); overflow: scroll"
|
||||
:class="{ 'is-active': show_player_menu }">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 12px 0;">
|
||||
|
||||
<!-- 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;">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading">Volume</p>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:value="player.volume"
|
||||
@change="set_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs: speaker volumes -->
|
||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||
|
||||
<!-- Outputs: stream volume -->
|
||||
<hr class="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>
|
||||
</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>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayerButtonPlayPause from './PlayerButtonPlayPause'
|
||||
import webapi from '@/webapi'
|
||||
import _audio from '@/audio'
|
||||
import NavbarItemLink from './NavbarItemLink'
|
||||
import NavbarItemOutput from './NavbarItemOutput'
|
||||
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause'
|
||||
import PlayerButtonNext from '@/components/PlayerButtonNext'
|
||||
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious'
|
||||
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle'
|
||||
import PlayerButtonConsume from '@/components/PlayerButtonConsume'
|
||||
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat'
|
||||
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack'
|
||||
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'NavbarBottom',
|
||||
components: { PlayerButtonPlayPause },
|
||||
components: {
|
||||
NavbarItemLink,
|
||||
NavbarItemOutput,
|
||||
RangeSlider,
|
||||
PlayerButtonPlayPause,
|
||||
PlayerButtonNext,
|
||||
PlayerButtonPrevious,
|
||||
PlayerButtonShuffle,
|
||||
PlayerButtonConsume,
|
||||
PlayerButtonRepeat,
|
||||
PlayerButtonSeekForward,
|
||||
PlayerButtonSeekBack
|
||||
},
|
||||
|
||||
data () {
|
||||
return { }
|
||||
return {
|
||||
old_volume: 0,
|
||||
|
||||
playing: false,
|
||||
loading: false,
|
||||
stream_volume: 10,
|
||||
|
||||
show_outputs_menu: false,
|
||||
show_desktop_outputs_menu: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
show_player_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_burger_menu () {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
|
||||
zindex () {
|
||||
if (this.show_burger_menu) {
|
||||
return 'z-index: 20'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
state () {
|
||||
return this.$store.state.player
|
||||
},
|
||||
now_playing () {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
is_now_playing_page () {
|
||||
return this.$route.path === '/now-playing'
|
||||
},
|
||||
outputs () {
|
||||
return this.$store.state.outputs
|
||||
},
|
||||
|
||||
player () {
|
||||
return this.$store.state.player
|
||||
},
|
||||
|
||||
config () {
|
||||
return this.$store.state.config
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
on_click_outside_outputs () {
|
||||
this.show_outputs_menu = false
|
||||
},
|
||||
|
||||
set_volume: function (newVolume) {
|
||||
webapi.player_volume(newVolume)
|
||||
},
|
||||
|
||||
toggle_mute_volume: function () {
|
||||
if (this.player.volume > 0) {
|
||||
this.set_volume(0)
|
||||
} else {
|
||||
this.set_volume(this.old_volume)
|
||||
}
|
||||
},
|
||||
|
||||
setupAudio: function () {
|
||||
const a = _audio.setupAudio()
|
||||
|
||||
a.addEventListener('waiting', e => {
|
||||
this.playing = false
|
||||
this.loading = true
|
||||
})
|
||||
a.addEventListener('playing', e => {
|
||||
this.playing = true
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('ended', e => {
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
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.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// close active audio
|
||||
closeAudio: function () {
|
||||
_audio.stopAudio()
|
||||
this.playing = false
|
||||
},
|
||||
|
||||
playChannel: function () {
|
||||
if (this.playing) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = '/stream.mp3'
|
||||
this.loading = true
|
||||
_audio.playSource(channel)
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
},
|
||||
|
||||
togglePlay: function () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.playing) {
|
||||
return this.closeAudio()
|
||||
}
|
||||
return this.playChannel()
|
||||
},
|
||||
|
||||
set_stream_volume: function (newVolume) {
|
||||
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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a class="navbar-item" :class="{ 'is-active': is_active }" @click.prevent="open_link()" :href="full_path()">
|
||||
<a class="navbar-item" :class="{ 'is-active': is_active }" @click.stop.prevent="open_link()" :href="full_path()">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
@ -9,17 +9,46 @@ import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'NavbarItemLink',
|
||||
props: [ 'to' ],
|
||||
props: {
|
||||
to: String,
|
||||
exact: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_active () {
|
||||
if (this.exact) {
|
||||
return this.$route.path === this.to
|
||||
}
|
||||
return this.$route.path.startsWith(this.to)
|
||||
},
|
||||
|
||||
show_player_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_link: function () {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, false)
|
||||
if (this.show_burger_menu) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, false)
|
||||
}
|
||||
if (this.show_player_menu) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, false)
|
||||
}
|
||||
this.$router.push({ path: this.to })
|
||||
},
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default {
|
||||
name: 'NavbarItemOutput',
|
||||
components: { RangeSlider },
|
||||
|
||||
props: [ 'output' ],
|
||||
props: ['output'],
|
||||
|
||||
computed: {
|
||||
type_class () {
|
||||
@ -61,7 +61,7 @@ export default {
|
||||
|
||||
set_enabled: function () {
|
||||
const values = {
|
||||
'selected': !this.output.selected
|
||||
selected: !this.output.selected
|
||||
}
|
||||
webapi.output_update(this.output.id, values)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<nav class="navbar is-light is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<nav class="navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<navbar-item-link to="/playlists">
|
||||
<span class="icon"><i class="mdi mdi-library-music"></i></span>
|
||||
@ -7,7 +7,7 @@
|
||||
<navbar-item-link to="/music">
|
||||
<span class="icon"><i class="mdi mdi-music"></i></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/podcasts" v-if="podcasts.tracks > 0">
|
||||
<navbar-item-link to="/podcasts">
|
||||
<span class="icon"><i class="mdi mdi-microphone"></i></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/audiobooks" v-if="audiobooks.tracks > 0">
|
||||
@ -20,7 +20,7 @@
|
||||
<span class="icon"><i class="mdi mdi-magnify"></i></span>
|
||||
</navbar-item-link>
|
||||
|
||||
<div class="navbar-burger" @click="update_show_burger_menu" :class="{ 'is-active': show_burger_menu }">
|
||||
<div class="navbar-burger" @click="show_burger_menu = !show_burger_menu" :class="{ 'is-active': show_burger_menu }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
@ -33,150 +33,59 @@
|
||||
|
||||
<div class="navbar-end">
|
||||
|
||||
<!-- Outputs dropdown -->
|
||||
<div class="navbar-item has-dropdown"
|
||||
:class="{ 'is-active': show_outputs_menu, 'is-hoverable': !show_outputs_menu && !show_settings_menu }"
|
||||
@click="show_outputs_menu = !show_outputs_menu"
|
||||
v-click-outside="on_click_outside_outputs">
|
||||
<a class="navbar-link is-arrowless"><span class="icon is-hidden-mobile is-hidden-tablet-only"><i class="mdi mdi-volume-high"></i></span> <span class="is-hidden-desktop has-text-weight-bold">Volume</span></a>
|
||||
|
||||
<div class="navbar-dropdown is-right">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading">Volume</p>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:value="player.volume"
|
||||
@change="set_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs: master volume -->
|
||||
<hr class="navbar-divider">
|
||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||
|
||||
<!-- Outputs: stream volume -->
|
||||
<hr class="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>
|
||||
<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>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playback controls -->
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div class="buttons has-addons">
|
||||
<player-button-previous class="button"></player-button-previous>
|
||||
<player-button-play-pause class="button"></player-button-play-pause>
|
||||
<player-button-next class="button"></player-button-next>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<div class="buttons has-addons">
|
||||
<player-button-repeat class="button is-light"></player-button-repeat>
|
||||
<player-button-shuffle class="button is-light"></player-button-shuffle>
|
||||
<player-button-consume class="button is-light"></player-button-consume>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings drop down -->
|
||||
<div class="navbar-item has-dropdown"
|
||||
:class="{ 'is-active': show_settings_menu, 'is-hoverable': !show_outputs_menu && !show_settings_menu }"
|
||||
@click="show_settings_menu = !show_settings_menu"
|
||||
v-click-outside="on_click_outside_settings">
|
||||
<a class="navbar-link is-arrowless"><span class="icon is-hidden-mobile is-hidden-tablet-only"><i class="mdi mdi-settings"></i></span> <span class="is-hidden-desktop has-text-weight-bold">forked-daapd</span></a>
|
||||
<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="is-hidden-desktop has-text-weight-bold">forked-daapd</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-right">
|
||||
|
||||
<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 style="padding-left: 1.5rem;">Artists</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/albums"><span style="padding-left: 1.5rem;">Albums</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/genres"><span style="padding-left: 1.5rem;">Genres</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span style="padding-left: 1.5rem;">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="/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 style="margin: 12px 0;">
|
||||
|
||||
<a class="navbar-item" href="/admin.html">Admin</a>
|
||||
<hr class="navbar-divider">
|
||||
<hr style="margin: 12px 0;">
|
||||
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
|
||||
<navbar-item-link to="/about">About</navbar-item-link>
|
||||
</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>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import _audio from '@/audio'
|
||||
import NavbarItemLink from './NavbarItemLink'
|
||||
import NavbarItemOutput from './NavbarItemOutput'
|
||||
import PlayerButtonPlayPause from './PlayerButtonPlayPause'
|
||||
import PlayerButtonNext from './PlayerButtonNext'
|
||||
import PlayerButtonPrevious from './PlayerButtonPrevious'
|
||||
import PlayerButtonShuffle from './PlayerButtonShuffle'
|
||||
import PlayerButtonConsume from './PlayerButtonConsume'
|
||||
import PlayerButtonRepeat from './PlayerButtonRepeat'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'NavbarTop',
|
||||
components: { NavbarItemLink, NavbarItemOutput, PlayerButtonPlayPause, PlayerButtonNext, PlayerButtonPrevious, PlayerButtonShuffle, PlayerButtonConsume, PlayerButtonRepeat, RangeSlider },
|
||||
components: { NavbarItemLink },
|
||||
|
||||
data () {
|
||||
return {
|
||||
old_volume: 0,
|
||||
|
||||
playing: false,
|
||||
loading: false,
|
||||
stream_volume: 10,
|
||||
|
||||
show_outputs_menu: false,
|
||||
show_settings_menu: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
outputs () {
|
||||
return this.$store.state.outputs
|
||||
},
|
||||
|
||||
player () {
|
||||
return this.$store.state.player
|
||||
},
|
||||
@ -197,108 +106,41 @@ export default {
|
||||
return this.$store.state.podcasts_count
|
||||
},
|
||||
|
||||
show_burger_menu () {
|
||||
return this.$store.state.show_burger_menu
|
||||
spotify_enabled () {
|
||||
return this.$store.state.spotify.webapi_token_valid
|
||||
},
|
||||
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_player_menu () {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
|
||||
zindex () {
|
||||
if (this.show_player_menu) {
|
||||
return 'z-index: 20'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update_show_burger_menu: function () {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, !this.show_burger_menu)
|
||||
},
|
||||
|
||||
on_click_outside_outputs () {
|
||||
this.show_outputs_menu = false
|
||||
},
|
||||
|
||||
on_click_outside_settings () {
|
||||
this.show_settings_menu = false
|
||||
},
|
||||
|
||||
set_volume: function (newVolume) {
|
||||
webapi.player_volume(newVolume)
|
||||
},
|
||||
|
||||
toggle_mute_volume: function () {
|
||||
if (this.player.volume > 0) {
|
||||
this.set_volume(0)
|
||||
} else {
|
||||
this.set_volume(this.old_volume)
|
||||
}
|
||||
},
|
||||
|
||||
setupAudio: function () {
|
||||
const a = _audio.setupAudio()
|
||||
|
||||
a.addEventListener('waiting', e => {
|
||||
this.playing = false
|
||||
this.loading = true
|
||||
})
|
||||
a.addEventListener('playing', e => {
|
||||
this.playing = true
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('ended', e => {
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
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.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// close active audio
|
||||
closeAudio: function () {
|
||||
_audio.stopAudio()
|
||||
this.playing = false
|
||||
},
|
||||
|
||||
playChannel: function () {
|
||||
if (this.playing) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = '/stream.mp3'
|
||||
this.loading = true
|
||||
_audio.playSource(channel)
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
},
|
||||
|
||||
togglePlay: function () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.playing) {
|
||||
return this.closeAudio()
|
||||
}
|
||||
return this.playChannel()
|
||||
},
|
||||
|
||||
set_stream_volume: function (newVolume) {
|
||||
this.stream_volume = newVolume
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
this.show_settings_menu = !this.show_settings_menu
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.player.volume' () {
|
||||
if (this.player.volume > 0) {
|
||||
this.old_volume = this.player.volume
|
||||
}
|
||||
$route (to, from) {
|
||||
this.show_settings_menu = false
|
||||
}
|
||||
},
|
||||
|
||||
// on app mounted
|
||||
mounted () {
|
||||
this.setupAudio()
|
||||
},
|
||||
|
||||
// on app destroyed
|
||||
destroyed () {
|
||||
this.closeAudio()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a v-on:click="toggle_consume_mode" v-bind:class="{ 'is-warning': is_consume }">
|
||||
<span class="icon"><i class="mdi mdi-fire"></i></span>
|
||||
<a @click="toggle_consume_mode" :class="{ 'is-warning': is_consume }">
|
||||
<span class="icon"><i class="mdi mdi-fire" :class="icon_style"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,10 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PlayerButtonConsume',
|
||||
|
||||
props: {
|
||||
icon_style: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_consume () {
|
||||
return this.$store.state.player.consume
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a v-on:click="play_next" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-forward"></i></span>
|
||||
<a @click="play_next" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-forward" :class="icon_style"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,10 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PlayerButtonNext',
|
||||
|
||||
props: {
|
||||
icon_style: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a @click="toggle_play_pause" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi" v-bind:class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing && is_pause_allowed, 'mdi-stop': is_playing && !is_pause_allowed }]"></i></span>
|
||||
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing && is_pause_allowed, 'mdi-stop': is_playing && !is_pause_allowed }]"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -11,8 +11,8 @@ export default {
|
||||
name: 'PlayerButtonPlayPause',
|
||||
|
||||
props: {
|
||||
'icon_style': String,
|
||||
'show_disabled_message': Boolean
|
||||
icon_style: String,
|
||||
show_disabled_message: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a v-on:click="play_previous" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-backward"></i></span>
|
||||
<a @click="play_previous" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-backward" :class="icon_style"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,10 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PlayerButtonPrevious',
|
||||
|
||||
props: {
|
||||
icon_style: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a v-on:click="toggle_repeat_mode" v-bind:class="{ 'is-warning': !is_repeat_off }">
|
||||
<span class="icon"><i class="mdi" v-bind:class="{ 'mdi-repeat': is_repeat_all, 'mdi-repeat-once': is_repeat_single, 'mdi-repeat-off': is_repeat_off }"></i></span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -10,8 +10,8 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PlayerButtonRepeat',
|
||||
|
||||
data () {
|
||||
return { }
|
||||
props: {
|
||||
icon_style: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
38
web-src/src/components/PlayerButtonSeekBack.vue
Normal file
38
web-src/src/components/PlayerButtonSeekBack.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||
<span class="icon"><i class="mdi mdi-rewind" :class="icon_style"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'PlayerButtonSeekBack',
|
||||
props: ['seek_ms', 'icon_style'],
|
||||
|
||||
computed: {
|
||||
now_playing () {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
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'
|
||||
},
|
||||
visible () {
|
||||
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
seek: function () {
|
||||
if (!this.disabled) {
|
||||
webapi.player_seek(this.seek_ms * -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
38
web-src/src/components/PlayerButtonSeekForward.vue
Normal file
38
web-src/src/components/PlayerButtonSeekForward.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||
<span class="icon"><i class="mdi mdi-fast-forward" :class="icon_style"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'PlayerButtonSeekForward',
|
||||
props: ['seek_ms', 'icon_style'],
|
||||
|
||||
computed: {
|
||||
now_playing () {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
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'
|
||||
},
|
||||
visible () {
|
||||
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
seek: function () {
|
||||
if (!this.disabled) {
|
||||
webapi.player_seek(this.seek_ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a v-on:click="toggle_shuffle_mode" v-bind:class="{ 'is-warning': is_shuffle }">
|
||||
<span class="icon"><i class="mdi" v-bind:class="{ 'mdi-shuffle': is_shuffle, 'mdi-shuffle-disabled': !is_shuffle }"></i></span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,10 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PlayerButtonShuffle',
|
||||
|
||||
props: {
|
||||
icon_style: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_shuffle () {
|
||||
return this.$store.state.player.shuffle
|
||||
|
113
web-src/src/components/SettingsCheckbox.vue
Normal file
113
web-src/src/components/SettingsCheckbox.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<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>
|
||||
</label>
|
||||
<p class="help" v-if="$slots['info']">
|
||||
<slot name="info"></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'SettingsCheckbox',
|
||||
|
||||
props: ['category_name', 'option_name'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerId: -1,
|
||||
|
||||
// <empty>: default/no changes, 'success': update succesful, 'error': update failed
|
||||
statusUpdate: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
category () {
|
||||
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
|
||||
},
|
||||
|
||||
option () {
|
||||
if (!this.category) {
|
||||
return {}
|
||||
}
|
||||
return this.category.options.find(elem => elem.name === this.option_name)
|
||||
},
|
||||
|
||||
value () {
|
||||
return this.option.value
|
||||
},
|
||||
|
||||
info () {
|
||||
if (this.statusUpdate === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdate === 'error') {
|
||||
return '(error saving setting)'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_update_timer () {
|
||||
if (this.timerId > 0) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = -1
|
||||
}
|
||||
|
||||
this.statusUpdate = ''
|
||||
const newValue = this.$refs.settings_checkbox.checked
|
||||
if (newValue !== this.value) {
|
||||
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
|
||||
}
|
||||
},
|
||||
|
||||
update_setting () {
|
||||
this.timerId = -1
|
||||
|
||||
const newValue = this.$refs.settings_checkbox.checked
|
||||
if (newValue === this.value) {
|
||||
this.statusUpdate = ''
|
||||
return
|
||||
}
|
||||
|
||||
const option = {
|
||||
category: this.category.name,
|
||||
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)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status: function () {
|
||||
this.statusUpdate = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
117
web-src/src/components/SettingsTextfield.vue
Normal file
117
web-src/src/components/SettingsTextfield.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<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>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" :placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="set_update_timer"
|
||||
ref="settings_text">
|
||||
</div>
|
||||
<p class="help" v-if="$slots['info']">
|
||||
<slot name="info"></slot>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'SettingsTextfield',
|
||||
|
||||
props: ['category_name', 'option_name', 'placeholder', 'disabled'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerId: -1,
|
||||
|
||||
// <empty>: default/no changes, 'success': update succesful, 'error': update failed
|
||||
statusUpdate: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
category () {
|
||||
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
|
||||
},
|
||||
|
||||
option () {
|
||||
if (!this.category) {
|
||||
return {}
|
||||
}
|
||||
return this.category.options.find(elem => elem.name === this.option_name)
|
||||
},
|
||||
|
||||
value () {
|
||||
return this.option.value
|
||||
},
|
||||
|
||||
info () {
|
||||
if (this.statusUpdate === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdate === 'error') {
|
||||
return '(error saving setting)'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_update_timer () {
|
||||
if (this.timerId > 0) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = -1
|
||||
}
|
||||
|
||||
this.statusUpdate = ''
|
||||
const newValue = this.$refs.settings_text.value
|
||||
if (newValue !== this.value) {
|
||||
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
|
||||
}
|
||||
},
|
||||
|
||||
update_setting () {
|
||||
this.timerId = -1
|
||||
|
||||
const newValue = this.$refs.settings_text.value
|
||||
if (newValue === this.value) {
|
||||
this.statusUpdate = ''
|
||||
return
|
||||
}
|
||||
|
||||
const option = {
|
||||
category: this.category.name,
|
||||
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)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status: function () {
|
||||
this.statusUpdate = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -51,7 +51,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'SpotifyModalDialogAlbum',
|
||||
props: [ 'show', 'album' ],
|
||||
props: ['show', 'album'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
|
@ -44,7 +44,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'SpotifyModalDialogArtist',
|
||||
props: [ 'show', 'artist' ],
|
||||
props: ['show', 'artist'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -48,7 +48,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'SpotifyModalDialogPlaylist',
|
||||
props: [ 'show', 'playlist' ],
|
||||
props: ['show', 'playlist'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -63,7 +63,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'SpotifyModalDialogTrack',
|
||||
props: [ 'show', 'track', 'album' ],
|
||||
props: ['show', 'track', 'album'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -15,6 +15,11 @@
|
||||
<span class="">Remotes & Outputs</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" to="/settings/artwork" active-class="is-active">
|
||||
<a>
|
||||
<span class="">Artwork</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" to="/settings/online-services" active-class="is-active">
|
||||
<a>
|
||||
<span class="">Online Services</span>
|
||||
|
31
web-src/src/lib/SVGRenderer.js
Normal file
31
web-src/src/lib/SVGRenderer.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SVGRenderer taken from https://github.com/bendera/placeholder published under MIT License
|
||||
* Copyright (c) 2017 Adam Bender
|
||||
* https://github.com/bendera/placeholder/blob/master/LICENSE
|
||||
*/
|
||||
class SVGRenderer {
|
||||
render (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)
|
||||
}
|
||||
}
|
||||
|
||||
export default SVGRenderer
|
@ -86,21 +86,6 @@ a.navbar-item {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fd-is-fullheight {
|
||||
height: calc(100vh - 3.25rem - 3.25rem);
|
||||
}
|
||||
|
||||
.fd-is-fullheight-body {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.fd-image-fullheight {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.fd-tabs-section {
|
||||
padding-bottom: 3px;
|
||||
padding-top: 3px;
|
||||
@ -123,6 +108,35 @@ section.fd-tabs-section + section.fd-content {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
/* Now playing page */
|
||||
.fd-is-fullheight {
|
||||
height: calc(100vh - 3.25rem - 3.25rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fd-is-fullheight .fd-is-expanded {
|
||||
max-height: calc(100vh - 25rem);
|
||||
padding: 1.5rem;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.fd-cover-image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fd-cover-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
filter: drop-shadow(0px 0px 1px rgba(0,0,0,.3)) drop-shadow(0px 0px 10px rgba(0,0,0,.3));
|
||||
}
|
||||
|
||||
|
||||
.sortable-chosen .media-right {
|
||||
visibility: hidden;
|
||||
}
|
||||
@ -180,3 +194,11 @@ section.fd-tabs-section + section.fd-content {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dropdown-item a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: hsl(0, 0%, 96%)
|
||||
}
|
||||
|
@ -24,9 +24,35 @@
|
||||
</div>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="level-right buttons">
|
||||
<a class="button is-small is-outlined is-link" :class="{ 'is-loading': library.updating }" @click="update">Update</a>
|
||||
<a class="button is-small is-outlined is-link" :class="{ 'is-loading': library.updating }" @click="update_meta">Force Meta Rescan</a>
|
||||
<div class="level-right">
|
||||
<div v-if="library.updating"><a class="button is-small is-loading">Update</a></div>
|
||||
<div v-else class="dropdown is-right" :class="{ 'is-active': show_update_dropdown }">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="buttons has-addons">
|
||||
<a @click="update" class="button is-small">Update</a>
|
||||
<a @click="show_update_dropdown = !show_update_dropdown" class="button is-small">
|
||||
<span class="icon"><i class="mdi" :class="{ 'mdi-chevron-down': !show_update_dropdown, 'mdi-chevron-up': show_update_dropdown }"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<div class="dropdown-item">
|
||||
<a @click="update" class="has-text-dark">
|
||||
<strong>Update</strong><br>
|
||||
<span class="is-size-7">Adds new, removes deleted and updates modified files.</span>
|
||||
</a>
|
||||
</div>
|
||||
<hr class="dropdown-divider">
|
||||
<div class="dropdown-item">
|
||||
<a @click="update_meta" class="has-text-dark">
|
||||
<strong>Rescan metadata</strong><br>
|
||||
<span class="is-size-7">Same as update, but also rescans unmodified files.</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -84,6 +110,12 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PageAbout',
|
||||
|
||||
data () {
|
||||
return {
|
||||
show_update_dropdown: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
config () {
|
||||
return this.$store.state.config
|
||||
@ -95,10 +127,12 @@ export default {
|
||||
|
||||
methods: {
|
||||
update: function () {
|
||||
this.show_update_dropdown = false
|
||||
webapi.library_update()
|
||||
},
|
||||
|
||||
update_meta: function () {
|
||||
this.show_update_dropdown = false
|
||||
webapi.library_rescan()
|
||||
}
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ const albumData = {
|
||||
|
||||
export default {
|
||||
name: 'PageAlbum',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -19,11 +19,10 @@
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in albums.items"
|
||||
<list-item-album v-for="album in albums_filtered"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
@click="open_album(album)"
|
||||
v-if="!hide_singles || album.track_count > 2">
|
||||
@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>
|
||||
@ -61,7 +60,7 @@ const albumsData = {
|
||||
|
||||
export default {
|
||||
name: 'PageAlbums',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbum, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
@ -77,6 +76,10 @@ export default {
|
||||
computed: {
|
||||
hide_singles () {
|
||||
return this.$store.state.hide_singles
|
||||
},
|
||||
|
||||
albums_filtered () {
|
||||
return this.albums.items.filter(album => !this.hide_singles || album.track_count > 2)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -52,7 +52,7 @@ const artistData = {
|
||||
|
||||
export default {
|
||||
name: 'PageArtist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(artistData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
|
@ -58,7 +58,7 @@ const tracksData = {
|
||||
|
||||
export default {
|
||||
name: 'PageArtistTracks',
|
||||
mixins: [ LoadDataBeforeEnterMixin(tracksData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
|
@ -19,11 +19,10 @@
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-artist v-for="artist in artists.items"
|
||||
<list-item-artist v-for="artist in artists_filtered"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
@click="open_artist(artist)"
|
||||
v-if="!hide_singles || artist.track_count > (artist.album_count * 2)">
|
||||
@click="open_artist(artist)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(artist)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
@ -58,7 +57,7 @@ const artistsData = {
|
||||
|
||||
export default {
|
||||
name: 'PageArtists',
|
||||
mixins: [ LoadDataBeforeEnterMixin(artistsData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
@ -79,6 +78,10 @@ export default {
|
||||
return [...new Set(this.artists.items
|
||||
.filter(artist => !this.$store.state.hide_singles || artist.track_count > (artist.album_count * 2))
|
||||
.map(artist => artist.name_sort.charAt(0).toUpperCase()))]
|
||||
},
|
||||
|
||||
artists_filtered () {
|
||||
return this.artists.items.filter(artist => !this.hide_singles || artist.track_count > (artist.album_count * 2))
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -56,7 +56,7 @@ const albumData = {
|
||||
|
||||
export default {
|
||||
name: 'PageAudiobook',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -38,7 +38,7 @@ const albumsData = {
|
||||
|
||||
export default {
|
||||
name: 'PageAudiobooks',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -80,7 +80,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'PageBrowse',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -45,7 +45,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'PageBrowseType',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -45,7 +45,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'PageBrowseType',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemTrack, ModalDialogTrack },
|
||||
|
||||
data () {
|
||||
|
@ -106,7 +106,7 @@ const filesData = {
|
||||
|
||||
export default {
|
||||
name: 'PageFiles',
|
||||
mixins: [ LoadDataBeforeEnterMixin(filesData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(filesData)],
|
||||
components: { ContentWithHeading, ListItemDirectory, ListItemPlaylist, ListItemTrack, ModalDialogDirectory, ModalDialogPlaylist, ModalDialogTrack },
|
||||
|
||||
data () {
|
||||
|
@ -36,7 +36,6 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemAlbums from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
@ -56,8 +55,8 @@ const genreData = {
|
||||
|
||||
export default {
|
||||
name: 'PageGenre',
|
||||
mixins: [ LoadDataBeforeEnterMixin(genreData) ],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbums, ModalDialogAlbum, ModalDialogGenre },
|
||||
mixins: [LoadDataBeforeEnterMixin(genreData)],
|
||||
components: { ContentWithHeading, IndexButtonList, ListItemAlbums, ModalDialogAlbum, ModalDialogGenre },
|
||||
|
||||
data () {
|
||||
return {
|
||||
|
@ -55,7 +55,7 @@ const tracksData = {
|
||||
|
||||
export default {
|
||||
name: 'PageGenreTracks',
|
||||
mixins: [ LoadDataBeforeEnterMixin(tracksData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogGenre },
|
||||
|
||||
data () {
|
||||
|
@ -45,7 +45,7 @@ const genresData = {
|
||||
|
||||
export default {
|
||||
name: 'PageGenres',
|
||||
mixins: [ LoadDataBeforeEnterMixin(genresData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(genresData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre },
|
||||
|
||||
data () {
|
||||
|
@ -1,83 +1,77 @@
|
||||
<template>
|
||||
<section class="hero fd-is-fullheight">
|
||||
<div class="hero-head fd-has-padding-left-right">
|
||||
<div class="container has-text-centered fd-has-margin-top">
|
||||
<h1 class="title is-4">
|
||||
{{ now_playing.title }}
|
||||
</h1>
|
||||
<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>
|
||||
<h3 class="subtitle is-6">
|
||||
{{ now_playing.album }}
|
||||
</h3>
|
||||
<section>
|
||||
<div v-if="now_playing.id > 0" class="fd-is-fullheight">
|
||||
<div class="fd-is-expanded">
|
||||
<cover-artwork @click="open_dialog(now_playing)"
|
||||
:artwork_url="now_playing.artwork_url"
|
||||
:artist="now_playing.artist"
|
||||
:album="now_playing.album"
|
||||
class="fd-cover-image fd-has-action" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="artwork_visible">
|
||||
<img :src="artwork_url" class="fd-has-shadow fd-image-fullheight fd-has-action"
|
||||
@load="artwork_loaded"
|
||||
@error="artwork_error"
|
||||
@click="open_dialog(now_playing)">
|
||||
</div>
|
||||
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="!artwork_visible">
|
||||
<a @click="open_dialog(now_playing)" class="button is-white is-medium">
|
||||
<span class="icon has-text-grey-light"><i class="mdi mdi-information-outline"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero-foot fd-has-padding-left-right">
|
||||
<div class="container has-text-centered fd-has-margin-bottom">
|
||||
<p class="control has-text-centered fd-progress-now-playing">
|
||||
<range-slider
|
||||
class="seek-slider fd-has-action"
|
||||
min="0"
|
||||
:max="state.item_length_ms"
|
||||
:value="item_progress_ms"
|
||||
:disabled="state.state === 'stop'"
|
||||
step="1000"
|
||||
@change="seek" >
|
||||
</range-slider>
|
||||
</p>
|
||||
<p class="content">
|
||||
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span>
|
||||
</p>
|
||||
<div class="buttons has-addons is-centered">
|
||||
<player-button-previous class="button is-medium"></player-button-previous>
|
||||
<player-button-play-pause class="button is-medium" icon_style="mdi-36px"></player-button-play-pause>
|
||||
<player-button-next class="button is-medium"></player-button-next>
|
||||
<player-button-repeat class="button is-medium is-light"></player-button-repeat>
|
||||
<player-button-shuffle class="button is-medium is-light"></player-button-shuffle>
|
||||
<player-button-consume class="button is-medium is-light"></player-button-consume>
|
||||
<div class="fd-has-padding-left-right">
|
||||
<div class="container has-text-centered">
|
||||
<p class="control has-text-centered fd-progress-now-playing">
|
||||
<range-slider
|
||||
class="seek-slider fd-has-action"
|
||||
min="0"
|
||||
:max="state.item_length_ms"
|
||||
:value="item_progress_ms"
|
||||
:disabled="state.state === 'stop'"
|
||||
step="1000"
|
||||
@change="seek" >
|
||||
</range-slider>
|
||||
</p>
|
||||
<p class="content">
|
||||
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-has-padding-left-right">
|
||||
<div class="container has-text-centered fd-has-margin-top">
|
||||
<h1 class="title is-5">
|
||||
{{ now_playing.title }}
|
||||
</h1>
|
||||
<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>
|
||||
<h3 class="subtitle is-6">
|
||||
{{ now_playing.album }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
|
||||
</div>
|
||||
<div v-else class="fd-is-fullheight" style="justify-content: center;">
|
||||
<div class="fd-is-expanded fd-has-padding-left-right has-text-centered">
|
||||
<h1 class="title is-5">
|
||||
You play queue is empty
|
||||
</h1>
|
||||
<p class="content">
|
||||
Add some tracks by browsing your library
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
|
||||
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause'
|
||||
import PlayerButtonNext from '@/components/PlayerButtonNext'
|
||||
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious'
|
||||
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle'
|
||||
import PlayerButtonConsume from '@/components/PlayerButtonConsume'
|
||||
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'PageNowPlaying',
|
||||
components: { ModalDialogQueueItem, PlayerButtonPlayPause, PlayerButtonNext, PlayerButtonPrevious, PlayerButtonShuffle, PlayerButtonConsume, PlayerButtonRepeat, RangeSlider },
|
||||
components: { ModalDialogQueueItem, RangeSlider, CoverArtwork },
|
||||
|
||||
data () {
|
||||
return {
|
||||
item_progress_ms: 0,
|
||||
interval_id: 0,
|
||||
artwork_visible: false,
|
||||
|
||||
show_details_modal: false,
|
||||
selected_item: {}
|
||||
@ -110,10 +104,6 @@ export default {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
|
||||
artwork_url: function () {
|
||||
return webapi.artwork_url_append_size_params(this.now_playing.artwork_url)
|
||||
},
|
||||
|
||||
settings_option_show_composer_now_playing () {
|
||||
return this.$store.getters.settings_option_show_composer_now_playing
|
||||
},
|
||||
@ -142,19 +132,11 @@ export default {
|
||||
},
|
||||
|
||||
seek: function (newPosition) {
|
||||
webapi.player_seek(newPosition).catch(() => {
|
||||
webapi.player_seek_to_pos(newPosition).catch(() => {
|
||||
this.item_progress_ms = this.state.item_progress_ms
|
||||
})
|
||||
},
|
||||
|
||||
artwork_loaded: function () {
|
||||
this.artwork_visible = true
|
||||
},
|
||||
|
||||
artwork_error: function () {
|
||||
this.artwork_visible = false
|
||||
},
|
||||
|
||||
open_dialog: function (item) {
|
||||
this.selected_item = item
|
||||
this.show_details_modal = true
|
||||
|
@ -52,7 +52,7 @@ const playlistData = {
|
||||
|
||||
export default {
|
||||
name: 'PagePlaylist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(playlistData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(playlistData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
|
@ -8,7 +8,7 @@
|
||||
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
||||
<template slot="icon">
|
||||
<span class="icon">
|
||||
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', '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' }"></i>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="actions">
|
||||
@ -25,7 +25,6 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import webapi from '@/webapi'
|
||||
@ -46,8 +45,8 @@ const playlistsData = {
|
||||
|
||||
export default {
|
||||
name: 'PagePlaylists',
|
||||
mixins: [ LoadDataBeforeEnterMixin(playlistsData) ],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemPlaylist, ModalDialogPlaylist },
|
||||
mixins: [LoadDataBeforeEnterMixin(playlistsData)],
|
||||
components: { ContentWithHeading, ListItemPlaylist, ModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
|
@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<div class="title is-4">{{ album.name }}</div>
|
||||
</template>
|
||||
<div class="title is-4">{{ album.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
||||
@ -35,8 +36,30 @@
|
||||
</a>
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" @play_count_changed="reload_tracks" />
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'podcast'" @close="show_album_details_modal = false" />
|
||||
<modal-dialog-track
|
||||
:show="show_details_modal"
|
||||
:track="selected_track"
|
||||
@close="show_details_modal = false"
|
||||
@play_count_changed="reload_tracks" />
|
||||
<modal-dialog-album
|
||||
:show="show_album_details_modal"
|
||||
:album="album"
|
||||
:media_kind="'podcast'"
|
||||
:new_tracks="new_tracks"
|
||||
@close="show_album_details_modal = false"
|
||||
@play_count_changed="reload_tracks"
|
||||
@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 slot="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>
|
||||
</template>
|
||||
</modal-dialog>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -47,6 +70,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialog from '@/components/ModalDialog'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -66,8 +90,8 @@ const albumData = {
|
||||
|
||||
export default {
|
||||
name: 'PagePodcast',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, RangeSlider, ModalDialogAlbum },
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, RangeSlider, ModalDialogAlbum, ModalDialog },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -77,7 +101,16 @@ export default {
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
show_album_details_modal: false,
|
||||
|
||||
show_remove_podcast_modal: false,
|
||||
rss_playlist_to_remove: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
new_tracks () {
|
||||
return this.tracks.filter(track => track.play_count === 0).length
|
||||
}
|
||||
},
|
||||
|
||||
@ -95,6 +128,27 @@ export default {
|
||||
this.show_details_modal = true
|
||||
},
|
||||
|
||||
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')
|
||||
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
|
||||
})
|
||||
},
|
||||
|
||||
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' })
|
||||
})
|
||||
},
|
||||
|
||||
reload_tracks: function () {
|
||||
webapi.library_podcast_episodes(this.album.id).then(({ data }) => {
|
||||
this.tracks = data.tracks.items
|
||||
|
@ -4,7 +4,17 @@
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">New episodes</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<template 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 slot="content">
|
||||
<list-item-track v-for="track in new_episodes.items" :key="track.id" :track="track" @click="play_track(track)">
|
||||
<template slot="progress">
|
||||
<range-slider
|
||||
@ -31,6 +41,16 @@
|
||||
<p class="title is-4">Podcasts</p>
|
||||
<p class="heading">{{ albums.total }} podcasts</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small" @click="open_add_podcast_dialog">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-rss"></i>
|
||||
</span>
|
||||
<span>Add Podcast</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'podcast'" @click="open_album(album)">
|
||||
<template slot="actions">
|
||||
@ -39,7 +59,28 @@
|
||||
</a>
|
||||
</template>
|
||||
</list-item-album>
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="selected_album" :media_kind="'podcast'" @close="show_album_details_modal = false" />
|
||||
<modal-dialog-album
|
||||
:show="show_album_details_modal"
|
||||
:album="selected_album"
|
||||
:media_kind="'podcast'"
|
||||
@close="show_album_details_modal = false"
|
||||
@play_count_changed="reload_new_episodes"
|
||||
@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 slot="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>
|
||||
</template>
|
||||
</modal-dialog>
|
||||
<modal-dialog-add-rss
|
||||
:show="show_url_modal"
|
||||
@close="show_url_modal = false"
|
||||
@podcast_added="reload_podcasts" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -52,6 +93,8 @@ import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialogAddRss from '@/components/ModalDialogAddRss'
|
||||
import ModalDialog from '@/components/ModalDialog'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -71,8 +114,8 @@ const albumsData = {
|
||||
|
||||
export default {
|
||||
name: 'PagePodcasts',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ListItemAlbum, ModalDialogTrack, ModalDialogAlbum, RangeSlider },
|
||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ListItemAlbum, ModalDialogTrack, ModalDialogAlbum, ModalDialogAddRss, ModalDialog, RangeSlider },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -82,8 +125,13 @@ export default {
|
||||
show_album_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
show_url_modal: false,
|
||||
|
||||
show_track_details_modal: false,
|
||||
selected_track: {}
|
||||
selected_track: {},
|
||||
|
||||
show_remove_podcast_modal: false,
|
||||
rss_playlist_to_remove: {}
|
||||
}
|
||||
},
|
||||
|
||||
@ -106,10 +154,51 @@ export default {
|
||||
this.show_album_details_modal = true
|
||||
},
|
||||
|
||||
mark_all_played: function () {
|
||||
this.new_episodes.items.forEach(ep => {
|
||||
webapi.library_track_update(ep.id, { play_count: 'increment' })
|
||||
})
|
||||
this.new_episodes.items = { }
|
||||
},
|
||||
|
||||
open_add_podcast_dialog: function (item) {
|
||||
this.show_url_modal = true
|
||||
},
|
||||
|
||||
open_remove_podcast_dialog: function () {
|
||||
this.show_album_details_modal = false
|
||||
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
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
remove_podcast: function () {
|
||||
this.show_remove_podcast_modal = false
|
||||
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => {
|
||||
this.reload_podcasts()
|
||||
})
|
||||
},
|
||||
|
||||
reload_new_episodes: function () {
|
||||
webapi.library_podcasts_new_episodes().then(({ data }) => {
|
||||
this.new_episodes = data.tracks
|
||||
})
|
||||
},
|
||||
|
||||
reload_podcasts: function () {
|
||||
webapi.library_podcasts().then(({ data }) => {
|
||||
this.albums = data
|
||||
this.reload_new_episodes()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,14 +18,6 @@
|
||||
</span>
|
||||
<span>Add Stream</span>
|
||||
</a>
|
||||
<!--
|
||||
<a class="button" :class="{ 'is-info': edit_mode }" @click="edit_mode = !edit_mode">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-content-save"></i>
|
||||
</span>
|
||||
<span>Save</span>
|
||||
</a>
|
||||
-->
|
||||
<a class="button is-small" :class="{ 'is-info': edit_mode }" @click="edit_mode = !edit_mode">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-pencil"></i>
|
||||
@ -150,7 +142,9 @@ export default {
|
||||
},
|
||||
|
||||
save_dialog: function (item) {
|
||||
this.show_pls_save_modal = true
|
||||
if (this.queue_items.length > 0) {
|
||||
this.show_pls_save_modal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,9 @@
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="help has-text-centered">Tip: you can search by a smart playlist query language <a href="https://github.com/ejurgensen/forked-daapd/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;">
|
||||
@ -148,6 +151,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
search_query: '',
|
||||
|
||||
tracks: { items: [], total: 0 },
|
||||
artists: { items: [], total: 0 },
|
||||
albums: { items: [], total: 0 },
|
||||
@ -210,9 +214,14 @@ export default {
|
||||
}
|
||||
|
||||
var searchParams = {
|
||||
'type': route.query.type,
|
||||
'query': route.query.query,
|
||||
'media_kind': 'music'
|
||||
type: route.query.type,
|
||||
media_kind: 'music'
|
||||
}
|
||||
|
||||
if (route.query.query.startsWith('query:')) {
|
||||
searchParams.expression = route.query.query.replace(/^query:/, '').trim()
|
||||
} else {
|
||||
searchParams.query = route.query.query
|
||||
}
|
||||
|
||||
if (route.query.limit) {
|
||||
@ -226,7 +235,7 @@ export default {
|
||||
this.albums = data.albums ? data.albums : { items: [], total: 0 }
|
||||
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 }
|
||||
|
||||
this.$store.commit(types.ADD_RECENT_SEARCH, searchParams.query)
|
||||
this.$store.commit(types.ADD_RECENT_SEARCH, route.query.query)
|
||||
})
|
||||
},
|
||||
|
||||
@ -235,7 +244,8 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({ path: '/search/library',
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'track,artist,album,playlist',
|
||||
query: this.search_query,
|
||||
@ -247,7 +257,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_tracks: function () {
|
||||
this.$router.push({ path: '/search/library',
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'track',
|
||||
query: this.$route.query.query
|
||||
@ -256,7 +267,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_artists: function () {
|
||||
this.$router.push({ path: '/search/library',
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'artist',
|
||||
query: this.$route.query.query
|
||||
@ -265,7 +277,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_albums: function () {
|
||||
this.$router.push({ path: '/search/library',
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'album',
|
||||
query: this.$route.query.query
|
||||
@ -274,7 +287,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_playlists: function () {
|
||||
this.$router.push({ path: '/search/library',
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'playlist',
|
||||
query: this.$route.query.query
|
||||
|
50
web-src/src/pages/SettingsPageArtwork.vue
Normal file
50
web-src/src/pages/SettingsPageArtwork.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-settings></tabs-settings>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<div class="title is-4">Artwork</div>
|
||||
</template>
|
||||
|
||||
<template slot="content">
|
||||
<div class="content">
|
||||
<p>
|
||||
forked-daapd 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>
|
||||
</div>
|
||||
<settings-checkbox category_name="artwork" option_name="use_artwork_source_spotify" v-if="spotify.enabled">
|
||||
<template slot="label"> Spotify</template>
|
||||
</settings-checkbox>
|
||||
<settings-checkbox category_name="artwork" option_name="use_artwork_source_discogs">
|
||||
<template slot="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 slot="label"> Cover Art Archive (<a href="https://coverartarchive.org/">https://coverartarchive.org/</a>)</template>
|
||||
</settings-checkbox>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsSettings from '@/components/TabsSettings'
|
||||
import SettingsCheckbox from '@/components/SettingsCheckbox'
|
||||
|
||||
export default {
|
||||
name: 'SettingsPageArtwork',
|
||||
components: { ContentWithHeading, TabsSettings, SettingsCheckbox },
|
||||
|
||||
computed: {
|
||||
spotify () {
|
||||
return this.$store.state.spotify
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -8,36 +8,15 @@
|
||||
</template>
|
||||
|
||||
<template slot="content">
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" :checked="settings_option_show_composer_now_playing" @change="set_timer_show_composer_now_playing" ref="checkbox_show_composer">
|
||||
Show composer
|
||||
<i class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdateShowComposerNowPlaying === 'success',
|
||||
'has-text-danger': statusUpdateShowComposerNowPlaying === 'error'
|
||||
}">{{ info_option_show_composer_now_playing }}</i>
|
||||
</label>
|
||||
<p class="help has-text-justified">
|
||||
If enabled the composer of the current playing track is shown on the "now playing page"
|
||||
</p>
|
||||
</div>
|
||||
<fieldset :disabled="!settings_option_show_composer_now_playing">
|
||||
<div class="field">
|
||||
<label class="label has-text-weight-normal">
|
||||
Show composer only for listed genres
|
||||
<i class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdateShowComposerForGenre === 'success',
|
||||
'has-text-danger': statusUpdateShowComposerForGenre === 'error'
|
||||
}">{{ info_option_show_composer_for_genre }}</i>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Genres"
|
||||
:value="settings_option_show_composer_for_genre"
|
||||
@input="set_timer_show_composer_for_genre"
|
||||
ref="field_composer_for_genre">
|
||||
</div>
|
||||
<settings-checkbox category_name="webinterface" option_name="show_composer_now_playing">
|
||||
<template slot="label"> Show composer</template>
|
||||
<template slot="info">If enabled the composer of the current playing track is shown on the "now playing page"</template>
|
||||
</settings-checkbox>
|
||||
<settings-textfield category_name="webinterface" option_name="show_composer_for_genre"
|
||||
:disabled="!settings_option_show_composer_now_playing"
|
||||
placeholder="Genres">
|
||||
<template slot="label">Show composer only for listed genres</template>
|
||||
<template slot="info">
|
||||
<p class="help">
|
||||
Comma separated list of genres the composer should be displayed on the "now playing page".
|
||||
</p>
|
||||
@ -49,8 +28,8 @@
|
||||
For example setting to <code>classical, soundtrack</code> will show the composer for tracks with
|
||||
a genre tag of "Contemporary Classical".<br>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
</settings-textfield>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -59,140 +38,17 @@
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsSettings from '@/components/TabsSettings'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import SettingsCheckbox from '@/components/SettingsCheckbox'
|
||||
import SettingsTextfield from '@/components/SettingsTextfield'
|
||||
|
||||
export default {
|
||||
name: 'SettingsPageWebinterface',
|
||||
components: { ContentWithHeading, TabsSettings },
|
||||
|
||||
data () {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerIdShowComposerNowPlaying: -1,
|
||||
timerIdShowComposerForGenre: -1,
|
||||
|
||||
// <empty>: default/no changes, 'success': update succesful, 'error': update failed
|
||||
statusUpdateShowComposerNowPlaying: '',
|
||||
statusUpdateShowComposerForGenre: ''
|
||||
}
|
||||
},
|
||||
components: { ContentWithHeading, TabsSettings, SettingsCheckbox, SettingsTextfield },
|
||||
|
||||
computed: {
|
||||
settings_category_webinterface () {
|
||||
return this.$store.getters.settings_webinterface
|
||||
},
|
||||
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
|
||||
},
|
||||
info_option_show_composer_for_genre () {
|
||||
if (this.statusUpdateShowComposerForGenre === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdateShowComposerForGenre === 'error') {
|
||||
return '(error saving setting)'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
info_option_show_composer_now_playing () {
|
||||
if (this.statusUpdateShowComposerNowPlaying === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdateShowComposerNowPlaying === 'error') {
|
||||
return '(error saving setting)'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_timer_show_composer_now_playing () {
|
||||
if (this.timerIdShowComposerNowPlaying > 0) {
|
||||
window.clearTimeout(this.timerIdShowComposerNowPlaying)
|
||||
this.timerIdShowComposerNowPlaying = -1
|
||||
}
|
||||
|
||||
this.statusUpdateShowComposerNowPlaying = ''
|
||||
const newValue = this.$refs.checkbox_show_composer.checked
|
||||
if (newValue !== this.settings_option_show_composer_now_playing) {
|
||||
this.timerIdShowComposerNowPlaying = window.setTimeout(this.update_show_composer_now_playing, this.timerDelay)
|
||||
}
|
||||
},
|
||||
|
||||
update_show_composer_now_playing () {
|
||||
this.timerIdShowComposerNowPlaying = -1
|
||||
|
||||
const newValue = this.$refs.checkbox_show_composer.checked
|
||||
if (newValue === this.settings_option_show_composer_now_playing) {
|
||||
this.statusUpdateShowComposerNowPlaying = ''
|
||||
return
|
||||
}
|
||||
|
||||
const option = {
|
||||
category: this.settings_category_webinterface.name,
|
||||
name: 'show_composer_now_playing',
|
||||
value: newValue
|
||||
}
|
||||
webapi.settings_update(this.settings_category_webinterface.name, option).then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdateShowComposerNowPlaying = 'success'
|
||||
}).catch(() => {
|
||||
this.statusUpdateShowComposerNowPlaying = 'error'
|
||||
this.$refs.checkbox_show_composer.checked = this.settings_option_show_composer_now_playing
|
||||
}).finally(() => {
|
||||
this.timerIdShowComposerNowPlaying = window.setTimeout(this.clear_status_show_composer_now_playing, this.timerDelay)
|
||||
})
|
||||
},
|
||||
|
||||
set_timer_show_composer_for_genre () {
|
||||
if (this.timerIdShowComposerForGenre > 0) {
|
||||
window.clearTimeout(this.timerIdShowComposerForGenre)
|
||||
this.timerIdShowComposerForGenre = -1
|
||||
}
|
||||
|
||||
this.statusUpdateShowComposerForGenre = ''
|
||||
const newValue = this.$refs.field_composer_for_genre.value
|
||||
if (newValue !== this.settings_option_show_composer_for_genre) {
|
||||
this.timerIdShowComposerForGenre = window.setTimeout(this.update_show_composer_for_genre, this.timerDelay)
|
||||
}
|
||||
},
|
||||
|
||||
update_show_composer_for_genre () {
|
||||
this.timerIdShowComposerForGenre = -1
|
||||
|
||||
const newValue = this.$refs.field_composer_for_genre.value
|
||||
if (newValue === this.settings_option_show_composer_for_genre) {
|
||||
this.statusUpdateShowComposerForGenre = ''
|
||||
return
|
||||
}
|
||||
|
||||
const option = {
|
||||
category: this.settings_category_webinterface.name,
|
||||
name: 'show_composer_for_genre',
|
||||
value: newValue
|
||||
}
|
||||
webapi.settings_update(this.settings_category_webinterface.name, option).then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdateShowComposerForGenre = 'success'
|
||||
}).catch(() => {
|
||||
this.statusUpdateShowComposerForGenre = 'error'
|
||||
this.$refs.field_composer_for_genre.value = this.settings_option_show_composer_for_genre
|
||||
}).finally(() => {
|
||||
this.timerIdShowComposerForGenre = window.setTimeout(this.clear_status_show_composer_for_genre, this.timerDelay)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status_show_composer_for_genre () {
|
||||
this.statusUpdateShowComposerForGenre = ''
|
||||
},
|
||||
|
||||
clear_status_show_composer_now_playing () {
|
||||
this.statusUpdateShowComposerNowPlaying = ''
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -53,7 +53,7 @@ const albumData = {
|
||||
|
||||
export default {
|
||||
name: 'PageAlbum',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -62,7 +62,7 @@ const artistData = {
|
||||
|
||||
export default {
|
||||
name: 'SpotifyPageArtist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(artistData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading },
|
||||
|
||||
data () {
|
||||
|
@ -92,7 +92,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'SpotifyPageBrowse',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
|
@ -50,7 +50,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'SpotifyPageBrowseFeaturedPlaylists',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemPlaylist, SpotifyModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
|
@ -50,7 +50,7 @@ const browseData = {
|
||||
|
||||
export default {
|
||||
name: 'SpotifyPageBrowseNewReleases',
|
||||
mixins: [ LoadDataBeforeEnterMixin(browseData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
|
@ -61,7 +61,7 @@ const playlistData = {
|
||||
|
||||
export default {
|
||||
name: 'SpotifyPagePlaylist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(playlistData) ],
|
||||
mixins: [LoadDataBeforeEnterMixin(playlistData)],
|
||||
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogPlaylist, InfiniteLoading },
|
||||
|
||||
data () {
|
||||
|
@ -178,7 +178,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
recent_searches () {
|
||||
return this.$store.state.recent_searches
|
||||
return this.$store.state.recent_searches.filter(search => !search.startsWith('query:'))
|
||||
},
|
||||
|
||||
show_tracks () {
|
||||
@ -222,7 +222,7 @@ export default {
|
||||
this.reset()
|
||||
|
||||
// If no search query present reset and focus search field
|
||||
if (!this.query.query || this.query.query === '') {
|
||||
if (!this.query.query || this.query.query === '' || this.query.query.startsWith('query:')) {
|
||||
this.search_query = ''
|
||||
this.$refs.search_field.focus()
|
||||
return
|
||||
@ -315,7 +315,8 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({ path: '/search/spotify',
|
||||
this.$router.push({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'track,artist,album,playlist',
|
||||
query: this.search_query,
|
||||
@ -327,7 +328,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_tracks: function () {
|
||||
this.$router.push({ path: '/search/spotify',
|
||||
this.$router.push({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'track',
|
||||
query: this.$route.query.query
|
||||
@ -336,7 +338,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_artists: function () {
|
||||
this.$router.push({ path: '/search/spotify',
|
||||
this.$router.push({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'artist',
|
||||
query: this.$route.query.query
|
||||
@ -345,7 +348,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_albums: function () {
|
||||
this.$router.push({ path: '/search/spotify',
|
||||
this.$router.push({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'album',
|
||||
query: this.$route.query.query
|
||||
@ -354,7 +358,8 @@ export default {
|
||||
},
|
||||
|
||||
open_search_playlists: function () {
|
||||
this.$router.push({ path: '/search/spotify',
|
||||
this.$router.push({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'playlist',
|
||||
query: this.$route.query.query
|
||||
|
@ -32,6 +32,7 @@ import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum'
|
||||
import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist'
|
||||
import SpotifyPageSearch from '@/pages/SpotifyPageSearch'
|
||||
import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface'
|
||||
import SettingsPageArtwork from '@/pages/SettingsPageArtwork'
|
||||
import SettingsPageOnlineServices from '@/pages/SettingsPageOnlineServices'
|
||||
import SettingsPageRemotesOutputs from '@/pages/SettingsPageRemotesOutputs'
|
||||
|
||||
@ -225,6 +226,11 @@ export const router = new VueRouter({
|
||||
name: 'Settings Webinterface',
|
||||
component: SettingsPageWebinterface
|
||||
},
|
||||
{
|
||||
path: '/settings/artwork',
|
||||
name: 'Settings Artwork',
|
||||
component: SettingsPageArtwork
|
||||
},
|
||||
{
|
||||
path: '/settings/online-services',
|
||||
name: 'Settings Online Services',
|
||||
@ -269,9 +275,15 @@ export const router = new VueRouter({
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const burgerMenuVisible = store.state.show_burger_menu
|
||||
if (burgerMenuVisible) {
|
||||
if (store.state.show_burger_menu) {
|
||||
store.commit(types.SHOW_BURGER_MENU, false)
|
||||
next(false)
|
||||
return
|
||||
}
|
||||
next(!burgerMenuVisible)
|
||||
if (store.state.show_player_menu) {
|
||||
store.commit(types.SHOW_PLAYER_MENU, false)
|
||||
next(false)
|
||||
return
|
||||
}
|
||||
next(true)
|
||||
})
|
||||
|
@ -7,37 +7,37 @@ Vue.use(Vuex)
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
config: {
|
||||
'websocket_port': 0,
|
||||
'version': '',
|
||||
'buildoptions': [ ]
|
||||
websocket_port: 0,
|
||||
version: '',
|
||||
buildoptions: []
|
||||
},
|
||||
settings: {
|
||||
'categories': []
|
||||
categories: []
|
||||
},
|
||||
library: {
|
||||
'artists': 0,
|
||||
'albums': 0,
|
||||
'songs': 0,
|
||||
'db_playtime': 0,
|
||||
'updating': false
|
||||
artists: 0,
|
||||
albums: 0,
|
||||
songs: 0,
|
||||
db_playtime: 0,
|
||||
updating: false
|
||||
},
|
||||
audiobooks_count: { },
|
||||
podcasts_count: { },
|
||||
outputs: [ ],
|
||||
outputs: [],
|
||||
player: {
|
||||
'state': 'stop',
|
||||
'repeat': 'off',
|
||||
'consume': false,
|
||||
'shuffle': false,
|
||||
'volume': 0,
|
||||
'item_id': 0,
|
||||
'item_length_ms': 0,
|
||||
'item_progress_ms': 0
|
||||
state: 'stop',
|
||||
repeat: 'off',
|
||||
consume: false,
|
||||
shuffle: false,
|
||||
volume: 0,
|
||||
item_id: 0,
|
||||
item_length_ms: 0,
|
||||
item_progress_ms: 0
|
||||
},
|
||||
queue: {
|
||||
'version': 0,
|
||||
'count': 0,
|
||||
'items': [ ]
|
||||
version: 0,
|
||||
count: 0,
|
||||
items: []
|
||||
},
|
||||
lastfm: {},
|
||||
spotify: {},
|
||||
@ -47,14 +47,15 @@ export default new Vuex.Store({
|
||||
spotify_featured_playlists: [],
|
||||
|
||||
notifications: {
|
||||
'next_id': 1,
|
||||
'list': []
|
||||
next_id: 1,
|
||||
list: []
|
||||
},
|
||||
recent_searches: [],
|
||||
|
||||
hide_singles: false,
|
||||
show_only_next_items: false,
|
||||
show_burger_menu: false
|
||||
show_burger_menu: false,
|
||||
show_player_menu: false
|
||||
},
|
||||
|
||||
getters: {
|
||||
@ -175,17 +176,20 @@ export default new Vuex.Store({
|
||||
},
|
||||
[types.SHOW_BURGER_MENU] (state, showBurgerMenu) {
|
||||
state.show_burger_menu = showBurgerMenu
|
||||
},
|
||||
[types.SHOW_PLAYER_MENU] (state, showPlayerMenu) {
|
||||
state.show_player_menu = showPlayerMenu
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
add_notification ({ commit, state }, notification) {
|
||||
const newNotification = {
|
||||
'id': state.notifications.next_id++,
|
||||
'type': notification.type,
|
||||
'text': notification.text,
|
||||
'topic': notification.topic,
|
||||
'timeout': notification.timeout
|
||||
id: state.notifications.next_id++,
|
||||
type: notification.type,
|
||||
text: notification.text,
|
||||
topic: notification.topic,
|
||||
timeout: notification.timeout
|
||||
}
|
||||
|
||||
commit(types.ADD_NOTIFICATION, newNotification)
|
||||
|
@ -21,3 +21,4 @@ export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH'
|
||||
export const HIDE_SINGLES = 'HIDE_SINGLES'
|
||||
export const SHOW_ONLY_NEXT_ITEMS = 'SHOW_ONLY_NEXT_ITEMS'
|
||||
export const SHOW_BURGER_MENU = 'SHOW_BURGER_MENU'
|
||||
export const SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU'
|
||||
|
@ -4,7 +4,9 @@ import store from '@/store'
|
||||
axios.interceptors.response.use(function (response) {
|
||||
return response
|
||||
}, function (error) {
|
||||
store.dispatch('add_notification', { text: 'Request failed (status: ' + error.request.status + ' ' + error.request.statusText + ', url: ' + error.request.responseURL + ')', type: 'danger' })
|
||||
if (error.request.status && error.request.responseURL) {
|
||||
store.dispatch('add_notification', { text: 'Request failed (status: ' + error.request.status + ' ' + error.request.statusText + ', url: ' + error.request.responseURL + ')', type: 'danger' })
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
@ -96,7 +98,7 @@ export default {
|
||||
},
|
||||
|
||||
queue_save_playlist (name) {
|
||||
return axios.post('/api/queue/save', undefined, { params: { 'name': name } }).then((response) => {
|
||||
return axios.post('/api/queue/save', undefined, { params: { name: name } }).then((response) => {
|
||||
store.dispatch('add_notification', { text: 'Queue saved to playlist "' + name + '"', type: 'info', timeout: 2000 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
@ -178,10 +180,14 @@ export default {
|
||||
return axios.put('/api/player/volume?volume=' + outputVolume + '&output_id=' + outputId)
|
||||
},
|
||||
|
||||
player_seek (newPosition) {
|
||||
player_seek_to_pos (newPosition) {
|
||||
return axios.put('/api/player/seek?position_ms=' + newPosition)
|
||||
},
|
||||
|
||||
player_seek (seekMs) {
|
||||
return axios.put('/api/player/seek?seek_ms=' + seekMs)
|
||||
},
|
||||
|
||||
outputs () {
|
||||
return axios.get('/api/outputs')
|
||||
},
|
||||
@ -213,8 +219,14 @@ export default {
|
||||
return axios.get('/api/library/albums/' + albumId)
|
||||
},
|
||||
|
||||
library_album_tracks (albumId) {
|
||||
return axios.get('/api/library/albums/' + albumId + '/tracks')
|
||||
library_album_tracks (albumId, filter = { limit: -1, offset: 0 }) {
|
||||
return axios.get('/api/library/albums/' + albumId + '/tracks', {
|
||||
params: filter
|
||||
})
|
||||
},
|
||||
|
||||
library_album_track_update (albumId, attributes) {
|
||||
return axios.put('/api/library/albums/' + albumId + '/tracks', undefined, { params: attributes })
|
||||
},
|
||||
|
||||
library_genres () {
|
||||
@ -223,9 +235,9 @@ export default {
|
||||
|
||||
library_genre (genre) {
|
||||
var genreParams = {
|
||||
'type': 'albums',
|
||||
'media_kind': 'music',
|
||||
'expression': 'genre is "' + genre + '"'
|
||||
type: 'albums',
|
||||
media_kind: 'music',
|
||||
expression: 'genre is "' + genre + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: genreParams
|
||||
@ -234,9 +246,9 @@ export default {
|
||||
|
||||
library_genre_tracks (genre) {
|
||||
var genreParams = {
|
||||
'type': 'tracks',
|
||||
'media_kind': 'music',
|
||||
'expression': 'genre is "' + genre + '"'
|
||||
type: 'tracks',
|
||||
media_kind: 'music',
|
||||
expression: 'genre is "' + genre + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: genreParams
|
||||
@ -246,8 +258,8 @@ export default {
|
||||
library_artist_tracks (artist) {
|
||||
if (artist) {
|
||||
var artistParams = {
|
||||
'type': 'tracks',
|
||||
'expression': 'songartistid is "' + artist + '"'
|
||||
type: 'tracks',
|
||||
expression: 'songartistid is "' + artist + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: artistParams
|
||||
@ -261,8 +273,8 @@ export default {
|
||||
|
||||
library_podcasts_new_episodes () {
|
||||
var episodesParams = {
|
||||
'type': 'tracks',
|
||||
'expression': 'media_kind is podcast and play_count = 0 ORDER BY time_added DESC'
|
||||
type: 'tracks',
|
||||
expression: 'media_kind is podcast and play_count = 0 ORDER BY time_added DESC'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: episodesParams
|
||||
@ -271,14 +283,22 @@ export default {
|
||||
|
||||
library_podcast_episodes (albumId) {
|
||||
var episodesParams = {
|
||||
'type': 'tracks',
|
||||
'expression': 'media_kind is podcast and songalbumid is "' + albumId + '" ORDER BY time_added DESC'
|
||||
type: 'tracks',
|
||||
expression: 'media_kind is podcast and songalbumid is "' + albumId + '" ORDER BY date_released DESC'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: episodesParams
|
||||
})
|
||||
},
|
||||
|
||||
library_add (url) {
|
||||
return axios.post('/api/library/add', undefined, { params: { url: url } })
|
||||
},
|
||||
|
||||
library_playlist_delete (playlistId) {
|
||||
return axios.delete('/api/library/playlists/' + playlistId, undefined)
|
||||
},
|
||||
|
||||
library_audiobooks () {
|
||||
return axios.get('/api/library/albums?media_kind=audiobook')
|
||||
},
|
||||
@ -303,12 +323,16 @@ export default {
|
||||
return axios.get('/api/library/tracks/' + trackId)
|
||||
},
|
||||
|
||||
library_track_playlists (trackId) {
|
||||
return axios.get('/api/library/tracks/' + trackId + '/playlists')
|
||||
},
|
||||
|
||||
library_track_update (trackId, attributes = {}) {
|
||||
return axios.put('/api/library/tracks/' + trackId, undefined, { params: attributes })
|
||||
},
|
||||
|
||||
library_files (directory = undefined) {
|
||||
var filesParams = { 'directory': directory }
|
||||
var filesParams = { directory: directory }
|
||||
return axios.get('/api/library/files', {
|
||||
params: filesParams
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user