[web] Migration to Vue 3 and Vite

This commit is contained in:
chme 2022-02-19 06:18:01 +01:00
parent 92279ef33d
commit de097fcf94
104 changed files with 2904 additions and 36569 deletions

View File

@ -1,4 +0,0 @@
> 0.25%
not ie <= 8
not dead
not op_mini all

View File

@ -1,2 +0,0 @@
VUE_APP_JSON_API_SERVER='http://localhost:3689'
VUE_APP_WEBSOCKET_SERVER='ws://localhost:3688'

View File

@ -1,17 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}

75
web-src/MIGRATION_VUE3.md Normal file
View File

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

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

20
web-src/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?ver2.0">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OwnTone</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

36455
web-src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,36 @@
{ {
"name": "owntone-web", "name": "owntone-web",
"version": "1.2.0", "version": "2.0.0",
"private": true,
"description": "OwnTone web interface",
"author": "chme <christian.meffert@googlemail.com>",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build --no-clean --modern", "build": "vite build",
"lint": "vue-cli-service lint", "preview": "vite preview",
"dev": "vue-cli-service serve" "serve": "vite"
}, },
"dependencies": { "dependencies": {
"@aacassandra/vue3-progressbar": "^1.0.3",
"@ts-pro/vue-eternal-loading": "^1.2.0",
"@vueform/slider": "^2.0.8",
"axios": "^0.25.0", "axios": "^0.25.0",
"bulma": "^0.9.3", "bulma": "^0.9.3",
"bulma-switch": "^2.0.4", "bulma-switch": "^2.0.4",
"core-js": "^3.15.2",
"mdi": "^2.2.43", "mdi": "^2.2.43",
"moment": "^2.29.1", "moment": "^2.29.1",
"moment-duration-format": "^2.3.2", "moment-duration-format": "^2.3.2",
"npm": "^7.19.1",
"reconnectingwebsocket": "^1.0.0", "reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2", "spotify-web-api-js": "^1.5.2",
"string-to-color": "^2.2.2", "string-to-color": "^2.2.2",
"v-click-outside": "^3.1.2", "vue": "^3.2.31",
"vue": "^2.6.14", "vue-router": "^4.0.12",
"vue-infinite-loading": "^2.4.5",
"vue-observe-visibility": "^1.0.0",
"vue-progressbar": "^0.7.5",
"vue-range-slider": "^0.6.0",
"vue-router": "^3.5.3",
"vue-scrollto": "^2.20.0", "vue-scrollto": "^2.20.0",
"vue-tiny-lazyload-img": "^0.1.0", "vue3-click-away": "^1.2.1",
"vuedraggable": "^2.24.3", "vue3-lazyload": "^0.2.5-beta",
"vuex": "^3.6.2" "vuedraggable": "^4.1.0",
"vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.5.15", "@vitejs/plugin-vue": "^2.2.0",
"@vue/cli-plugin-eslint": "^5.0.0-rc.1", "sass": "^1.49.7",
"@vue/cli-service": "^4.5.15", "vite": "^2.8.1"
"@vue/eslint-config-standard": "^6.1.0", }
"babel-eslint": "^10.1.0",
"eslint": "^7.30.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.12.1",
"sass": "^1.35.1",
"sass-loader": "^10",
"vue-template-compiler": "^2.6.14"
},
"license": "GPL-2.0"
} }

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
web-src/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,19 +0,0 @@
<!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.0">
<title>OwnTone Web</title>
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png?ver2.0">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

31
web-src/public/logo.svg Normal file
View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#107A66;}
.st1{fill:#479386;}
.st2{fill:#00D1B2;}
.st3{fill:#FFFFFF;}
</style>
<path class="st0" d="M390.5,193.9h30.3c18.5,49.6,13.9,104.8-12.7,150.6c-5.1,8.8-11,17.2-17.6,24.9V193.9z"/>
<path class="st1" d="M440.3,163.9h-79.8v269.6C453.8,378.5,488.6,260.8,440.3,163.9L440.3,163.9z"/>
<path class="st2" d="M256,432c-97,0-176-79-176-176S159,80,256,80c26.9-0.1,53.4,6.1,77.5,17.9H187.9v96h99.8v235.2
C277.2,431,266.6,432,256,432z"/>
<path class="st1" d="M157.9,147.8v76.1h99.8v178H256c-80.6,0-146-65.4-145.9-146.1c0-38.7,15.4-75.8,42.7-103.1
C154.4,151,156.1,149.4,157.9,147.8 M256,50C142.2,50,50,142.2,50,256s92.2,206,206,206c20.9,0,41.7-3.2,61.7-9.4V163.9h-99.8v-36
h199.4C378.3,78.6,318.8,49.9,256,50z"/>
<path class="st2" d="M429.7,183.5c19.7,46.8,19.7,99.6,0,146.4c-12.9,30.1-33.2,56.4-59.2,76.3v3.8c48.5-36,77-92.9,77-153.3
c0.1-28.4-6.3-56.4-18.5-82h-3.3C427.1,177.6,428.5,180.5,429.7,183.5z"/>
<path class="st2" d="M434,256.7c0.1-28.5-6.8-56.7-20-82h-3.4c2.4,4.6,4.7,9.3,6.7,14.1c18.3,43.4,18.3,92.4,0,135.8
c-8.8,20.7-21.5,39.6-37.4,55.5c-3,3-6.2,5.9-9.4,8.7v3.9C410.8,359,434,309.2,434,256.7z"/>
<path class="st2" d="M420.5,256.7c0-28.8-7.5-57.1-21.9-82h-3.5c3.7,6.2,6.9,12.7,9.8,19.3c25.6,60.3,11.9,130.1-34.4,176.4v4.2
C402.5,343.7,420.5,301.2,420.5,256.7z"/>
<path class="st2" d="M382.7,174.7h-3.5c5.2,7.8,9.6,16,13.2,24.6c21.3,50.5,12.9,108.7-21.9,151v4.7
C414.2,304.3,419.1,230.8,382.7,174.7z"/>
<path class="st2" d="M370.5,180.7v5.5c26.7,43.2,26.7,97.8,0,141v5.5C401.1,286.7,401.1,226.7,370.5,180.7z"/>
<g>
<path class="st3" d="M417.3,127.9H217.9v36h99.8v288.7c15.1-4.7,29.5-11.2,42.8-19.1V163.9h79.8C433.9,151.1,426.2,139,417.3,127.9
z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,42 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3173 6294 c-330 -40 -614 -124 -918 -274 -299 -147 -489 -284 -741
-535 -250 -249 -386 -439 -540 -750 -128 -260 -212 -532 -261 -845 -26 -164
-26 -616 0 -780 87 -555 303 -1035 647 -1435 450 -523 1045 -852 1740 -960
141 -22 515 -31 662 -16 159 17 324 46 461 83 l117 31 0 1973 0 1974 -680 0
-680 0 0 250 0 250 1356 0 1355 0 -22 33 c-12 17 -97 106 -188 197 -250 249
-439 385 -746 535 -310 152 -615 240 -954 274 -140 15 -470 12 -608 -5z m-333
-2354 l680 0 0 -1215 0 -1215 -79 0 c-261 0 -571 79 -836 212 -187 94 -331
197 -488 347 -391 375 -607 885 -607 1431 0 531 200 1020 572 1400 l73 75 3
-517 2 -518 680 0z"/>
<path d="M4930 2920 l0 -1841 23 12 c43 23 193 127 287 199 136 104 389 362
496 505 298 398 476 825 550 1317 21 145 30 505 15 654 -32 315 -117 629 -244
907 l-40 87 -543 0 -544 0 0 -1840z m318 1662 c38 -53 131 -237 168 -332 90
-231 133 -441 141 -686 9 -264 -21 -479 -103 -724 -74 -222 -168 -402 -313
-594 -60 -80 -66 -86 -69 -61 -3 18 8 41 36 78 518 678 555 1608 94 2325 -13
20 -13 22 6 22 11 0 30 -13 40 -28z m234 -34 c134 -260 208 -493 249 -788 18
-128 15 -432 -6 -575 -67 -464 -269 -887 -584 -1225 -63 -67 -70 -72 -71 -50
0 18 21 50 71 105 370 411 569 927 569 1473 0 343 -73 659 -223 962 -39 80
-73 148 -75 153 -2 4 6 7 17 7 15 0 29 -16 53 -62z m234 -58 c89 -197 151
-405 191 -645 25 -153 25 -542 0 -700 -87 -556 -349 -1057 -743 -1423 -74 -69
-90 -79 -92 -63 -2 15 27 51 105 128 366 365 593 798 684 1308 27 150 37 491
20 646 -32 283 -110 556 -222 781 -22 43 -39 80 -39 83 0 4 10 5 22 3 18 -2
33 -26 74 -118z m193 18 c382 -906 225 -1946 -407 -2698 -91 -109 -242 -257
-354 -348 -71 -58 -78 -61 -78 -40 0 18 27 47 112 119 177 150 371 375 505
586 85 136 207 394 257 543 49 150 86 307 113 480 23 154 23 527 0 683 -36
236 -101 471 -183 663 -24 57 -44 106 -44 109 0 3 8 5 18 5 13 0 29 -27 61
-102z m-775 -95 c107 -189 188 -424 222 -639 25 -165 23 -424 -5 -588 -40
-231 -111 -430 -219 -620 -50 -85 -57 -95 -60 -71 -3 19 16 65 56 144 110 212
168 393 197 617 49 376 -30 795 -211 1118 -24 43 -44 91 -44 107 0 16 2 29 4
29 3 0 30 -44 60 -97z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

@ -2,10 +2,10 @@
<div id="app"> <div id="app">
<navbar-top /> <navbar-top />
<vue-progress-bar class="fd-progress-bar" /> <vue-progress-bar class="fd-progress-bar" />
<transition name="fade"> <router-view v-slot="{ Component }">
<!-- Setting v-show to true on the router-view tag avoids jumpiness during transitions --> <component :is="Component" class="fd-page" />
<router-view v-show="true" /> </router-view>
</transition>
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" /> <modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" />
<modal-dialog-update <modal-dialog-update
:show="show_update_dialog" :show="show_update_dialog"
@ -18,11 +18,11 @@
</template> </template>
<script> <script>
import NavbarTop from '@/components/NavbarTop' import NavbarTop from '@/components/NavbarTop.vue'
import NavbarBottom from '@/components/NavbarBottom' import NavbarBottom from '@/components/NavbarBottom.vue'
import Notifications from '@/components/Notifications' import Notifications from '@/components/Notifications.vue'
import ModalDialogRemotePairing from '@/components/ModalDialogRemotePairing' import ModalDialogRemotePairing from '@/components/ModalDialogRemotePairing.vue'
import ModalDialogUpdate from '@/components/ModalDialogUpdate' import ModalDialogUpdate from '@/components/ModalDialogUpdate.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import ReconnectingWebSocket from 'reconnectingwebsocket' import ReconnectingWebSocket from 'reconnectingwebsocket'
@ -125,9 +125,9 @@ export default {
} }
let wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port let wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port
if (process.env.NODE_ENV === 'development' && process.env.VUE_APP_WEBSOCKET_SERVER) { if (import.meta.env.NODE_ENV === 'development' && import.meta.env.VUE_APP_WEBSOCKET_SERVER) {
// If we are running in the development server, use the websocket url configured in .env.development // If we are running in the development server, use the websocket url configured in .env.development
wsUrl = process.env.VUE_APP_WEBSOCKET_SERVER wsUrl = import.meta.env.VUE_APP_WEBSOCKET_SERVER
} }
const socket = new ReconnectingWebSocket( const socket = new ReconnectingWebSocket(

View File

@ -1,17 +1,13 @@
<template> <template>
<figure> <figure>
<img v-lazyload <img v-lazy="{ src: artwork_url_with_size, lifecycle: lazy_lifecycle }"
:data-src="artwork_url_with_size"
:data-err="dataURI"
:key="artwork_url_with_size"
@click="$emit('click')"> @click="$emit('click')">
</figure> </figure>
</template> </template>
<script> <script>
import webapi from '@/webapi' import webapi from '@/webapi'
import SVGRenderer from '@/lib/SVGRenderer' import { renderSVG } from '@/lib/SVGRenderer'
import stringToColor from 'string-to-color'
export default { export default {
name: 'CoverArtwork', name: 'CoverArtwork',
@ -19,12 +15,16 @@ export default {
data () { data () {
return { return {
svg: new SVGRenderer(),
width: 600, width: 600,
height: 600, height: 600,
font_family: 'sans-serif', font_family: 'sans-serif',
font_size: 200, font_size: 200,
font_weight: 600 font_weight: 600,
lazy_lifecycle: {
error: (el) => {
el.src = this.dataURI()
}
}
} }
}, },
@ -48,47 +48,18 @@ export default {
return this.artist.substring(0, 2) return this.artist.substring(0, 2)
} }
return '' 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 () { methods: {
return this.svg.render(this.rendererParams) dataURI: function () {
return renderSVG(this.caption, this.alt_text, {
width: this.width,
height: this.height,
font_family: this.font_family,
font_size: this.font_size,
font_weight: this.font_weight
})
} }
} }
} }

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="dropdown" :class="{ 'is-active': is_active }" v-click-outside="onClickOutside"> <div class="dropdown" :class="{ 'is-active': is_active }" v-click-away="onClickOutside">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" @click="is_active = !is_active"> <button class="button" aria-haspopup="true" aria-controls="dropdown-menu" @click="is_active = !is_active">
<span>{{ value }}</span> <span>{{ modelValue }}</span>
<span class="icon is-small"> <span class="icon is-small">
<i class="mdi mdi-chevron-down" aria-hidden="true"></i> <i class="mdi mdi-chevron-down" aria-hidden="true"></i>
</span> </span>
@ -12,7 +12,7 @@
<div class="dropdown-content"> <div class="dropdown-content">
<a class="dropdown-item" <a class="dropdown-item"
v-for="option in options" :key="option" v-for="option in options" :key="option"
:class="{'is-active': value === option}" :class="{'is-active': modelValue === option}"
@click="select(option)"> @click="select(option)">
{{ option }} {{ option }}
</a> </a>
@ -25,7 +25,8 @@
export default { export default {
name: 'DropdownMenu', name: 'DropdownMenu',
props: ['value', 'options'], props: ['modelValue', 'options'],
emits: ['update:modelValue'],
data () { data () {
return { return {
@ -40,7 +41,7 @@ export default {
select (option) { select (option) {
this.is_active = false this.is_active = false
this.$emit('input', option) this.$emit('update:modelValue', option)
} }
} }
} }

View File

@ -21,7 +21,7 @@ export default {
methods: { methods: {
nav: function (id) { nav: function (id) {
this.$router.push({ path: this.$router.currentRoute.path + '#index_' + id }) this.$router.push({ hash: '#index_' + id })
}, },
scroll_to_top: function () { scroll_to_top: function () {

View File

@ -3,11 +3,13 @@
<div v-if="is_grouped"> <div v-if="is_grouped">
<div v-for="idx in albums.indexList" :key="idx" class="mb-6"> <div v-for="idx in albums.indexList" :key="idx" class="mb-6">
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span> <span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
<list-item-album v-for="album in albums.grouped[idx]"
<div class="media" v-for="album in albums.grouped[idx]"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <div class="media-left fd-has-action"
v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
@ -16,13 +18,28 @@
:maxwidth="64" :maxwidth="64"
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </div>
<template slot="actions"> <div class="media-content fd-has-action is-clipped">
<a @click="open_dialog(album)"> <div style="margin-top:0.7rem;">
<h1 class="title is-6">{{ album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal"
v-if="album.date_released && album.media_kind === 'music'">
{{ $filters.time(album.date_released, 'L') }}
</h2>
</div>
</div>
<div class="media-right" style="padding-top:0.7rem;">
<a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </div>
</list-item-album> </div>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@ -30,7 +47,7 @@
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <template v-slot:artwork v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
@ -40,8 +57,8 @@
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(album)"> <a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -60,7 +77,7 @@
delete_action="Remove" delete_action="Remove"
@close="show_remove_podcast_modal = false" @close="show_remove_podcast_modal = false"
@delete="remove_podcast"> @delete="remove_podcast">
<template slot="modal-content"> <template v-slot:modal-content>
<p>Permanently remove this podcast from your library?</p> <p>Permanently remove this podcast from your library?</p>
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p> <p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p>
</template> </template>
@ -69,10 +86,10 @@
</template> </template>
<script> <script>
import ListItemAlbum from '@/components/ListItemAlbum' import ListItemAlbum from '@/components/ListItemAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialog from '@/components/ModalDialog' import ModalDialog from '@/components/ModalDialog.vue'
import CoverArtwork from '@/components/CoverArtwork' import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
import Albums from '@/lib/Albums' import Albums from '@/lib/Albums'
@ -105,7 +122,10 @@ export default {
if (Array.isArray(this.albums)) { if (Array.isArray(this.albums)) {
return this.albums return this.albums
} }
if (this.albums) {
return this.albums.sortedAndFiltered return this.albums.sortedAndFiltered
}
return []
}, },
is_grouped: function () { is_grouped: function () {

View File

@ -7,8 +7,8 @@
:key="artist.id" :key="artist.id"
:artist="artist" :artist="artist"
@click="open_artist(artist)"> @click="open_artist(artist)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(artist)"> <a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -20,8 +20,8 @@
:key="artist.id" :key="artist.id"
:artist="artist" :artist="artist"
@click="open_artist(artist)"> @click="open_artist(artist)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(artist)"> <a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -32,8 +32,8 @@
</template> </template>
<script> <script>
import ListItemArtist from '@/components/ListItemArtist' import ListItemArtist from '@/components/ListItemArtist.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import Artists from '@/lib/Artists' import Artists from '@/lib/Artists'
export default { export default {

View File

@ -7,8 +7,8 @@
:key="composer.id" :key="composer.id"
:composer="composer" :composer="composer"
@click="open_composer(composer)"> @click="open_composer(composer)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(composer)"> <a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -20,8 +20,8 @@
:key="composer.id" :key="composer.id"
:composer="composer" :composer="composer"
@click="open_composer(composer)"> @click="open_composer(composer)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(composer)"> <a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -32,8 +32,8 @@
</template> </template>
<script> <script>
import ListItemComposer from '@/components/ListItemComposer' import ListItemComposer from '@/components/ListItemComposer.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer' import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import Composers from '@/lib/Composers' import Composers from '@/lib/Composers'
export default { export default {

View File

@ -1,17 +1,16 @@
<template functional> <template>
<div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()"> <div class="media" :id="'index_' + album.name_sort.charAt(0).toUpperCase()">
<div class="media-left fd-has-action" <div class="media-left fd-has-action"
v-if="$slots['artwork']" v-if="$slots['artwork']">
@click="listeners.click">
<slot name="artwork"></slot> <slot name="artwork"></slot>
</div> </div>
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<div style="margin-top:0.7rem;"> <div style="margin-top:0.7rem;">
<h1 class="title is-6">{{ props.album.name }}</h1> <h1 class="title is-6">{{ album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artist }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal" <h2 class="subtitle is-7 has-text-grey has-text-weight-normal"
v-if="props.album.date_released && props.album.media_kind === 'music'"> v-if="album.date_released && album.media_kind === 'music'">
{{ props.album.date_released | time('L') }} {{ $filters.time(album.date_released, 'L') }}
</h2> </h2>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template functional> <template>
<div class="media"> <div class="media">
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.artist.name }}</h1> <h1 class="title is-6">{{ artist.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -1,7 +1,7 @@
<template functional> <template>
<div class="media" :id="'index_' + props.composer.name.charAt(0).toUpperCase()"> <div class="media" :id="'index_' + composer.name.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.composer.name }}</h1> <h1 class="title is-6">{{ composer.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -1,13 +1,13 @@
<template functional> <template>
<div class="media"> <div class="media">
<figure class="media-left fd-has-action" @click="listeners.click"> <figure class="media-left fd-has-action">
<span class="icon"> <span class="icon">
<i class="mdi mdi-folder"></i> <i class="mdi mdi-folder"></i>
</span> </span>
</figure> </figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.directory.path.substring(props.directory.path.lastIndexOf('/') + 1) }}</h1> <h1 class="title is-6">{{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}</h1>
<h2 class="subtitle is-7 has-text-grey-light">{{ props.directory.path }}</h2> <h2 class="subtitle is-7 has-text-grey-light">{{ directory.path }}</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -1,7 +1,7 @@
<template functional> <template>
<div class="media" :id="'index_' + props.genre.name.charAt(0).toUpperCase()"> <div class="media" :id="'index_' + genre.name.charAt(0).toUpperCase()">
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.genre.name }}</h1> <h1 class="title is-6">{{ genre.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -1,10 +1,10 @@
<template functional> <template>
<div class="media"> <div class="media">
<figure class="media-left fd-has-action" v-if="slots().icon" @click="listeners.click"> <figure class="media-left fd-has-action" v-if="$slots.icon">
<slot name="icon"></slot> <slot name="icon"></slot>
</figure> </figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.playlist.name }}</h1> <h1 class="title is-6">{{ playlist.name }}</h1>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -1,12 +1,12 @@
<template functional> <template>
<div class="media" :id="'index_' + props.track.title_sort.charAt(0).toUpperCase()" :class="{ 'with-progress': slots().progress }"> <div class="media" :id="'index_' + track.title_sort.charAt(0).toUpperCase()" :class="{ 'with-progress': $slots.progress }">
<figure class="media-left fd-has-action" v-if="slots().icon" @click="listeners.click"> <figure class="media-left fd-has-action" v-if="$slots.icon">
<slot name="icon"></slot> <slot name="icon"></slot>
</figure> </figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6" :class="{ 'has-text-grey': props.track.media_kind === 'podcast' && props.track.play_count > 0 }">{{ props.track.title }}</h1> <h1 class="title is-6" :class="{ 'has-text-grey': track.media_kind === 'podcast' && track.play_count > 0 }">{{ track.title }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.track.artist }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ track.artist }}</b></h2>
<h2 class="subtitle is-7 has-text-grey">{{ props.track.album }}</h2> <h2 class="subtitle is-7 has-text-grey">{{ track.album }}</h2>
<slot name="progress"></slot> <slot name="progress"></slot>
</div> </div>
<div class="media-right"> <div class="media-right">

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)"> <list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template slot="icon"> <template v-slot:icon>
<span class="icon"> <span class="icon">
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i> <i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i>
</span> </span>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(playlist)"> <a @click.prevent.stop="open_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -17,8 +17,8 @@
</template> </template>
<script> <script>
import ListItemPlaylist from '@/components/ListItemPlaylist' import ListItemPlaylist from '@/components/ListItemPlaylist.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist' import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
export default { export default {
name: 'ListPlaylists', name: 'ListPlaylists',

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index, track)"> <list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index, track)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -12,8 +12,8 @@
</template> </template>
<script> <script>
import ListItemTrack from '@/components/ListItemTrack' import ListItemTrack from '@/components/ListItemTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {

View File

@ -25,7 +25,7 @@
</p> </p>
<p v-if="album.date_released"> <p v-if="album.date_released">
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ album.date_released | time('L') }}</span> <span class="title is-6">{{ $filters.time(album.date_released, 'L') }}</span>
</p> </p>
<p v-else-if="album.year > 0"> <p v-else-if="album.year > 0">
<span class="heading">Year</span> <span class="heading">Year</span>
@ -37,7 +37,7 @@
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ album.length_ms | duration }}</span> <span class="title is-6">{{ $filters.duration(album.length_ms) }}</span>
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>
@ -45,7 +45,7 @@
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ album.time_added | time('L LT') }}</span> <span class="title is-6">{{ $filters.time(album.time_added, 'L LT') }}</span>
</p> </p>
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
</template> </template>
<script> <script>
import CoverArtwork from '@/components/CoverArtwork' import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {

View File

@ -24,7 +24,7 @@
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ artist.time_added | time('L LT') }}</span> <span class="title is-6">{{ $filters.time(artist.time_added, 'L LT') }}</span>
</p> </p>
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ item.length_ms | duration }}</span> <span class="title is-6">{{ $filters.duration(item.length_ms) }}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>
@ -56,7 +56,7 @@
<span class="title is-6"> <span class="title is-6">
{{ item.type }} {{ item.type }}
<span v-if="item.samplerate"> | {{ item.samplerate }} Hz</span> <span v-if="item.samplerate"> | {{ item.samplerate }} Hz</span>
<span v-if="item.channels"> | {{ item.channels | channels }}</span> <span v-if="item.channels"> | {{ $filters.channels(item.channels) }}</span>
<span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span> <span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span>
</span> </span>
</p> </p>

View File

@ -31,7 +31,7 @@
</p> </p>
<p v-if="track.date_released"> <p v-if="track.date_released">
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ track.date_released | time('L') }}</span> <span class="title is-6">{{ $filters.time(track.date_released, 'L') }}</span>
</p> </p>
<p v-else-if="track.year > 0"> <p v-else-if="track.year > 0">
<span class="heading">Year</span> <span class="heading">Year</span>
@ -47,7 +47,7 @@
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ track.length_ms | duration }}</span> <span class="title is-6">{{ $filters.duration(track.length_ms) }}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>
@ -62,13 +62,13 @@
<span class="title is-6"> <span class="title is-6">
{{ track.type }} {{ track.type }}
<span v-if="track.samplerate"> | {{ track.samplerate }} Hz</span> <span v-if="track.samplerate"> | {{ track.samplerate }} Hz</span>
<span v-if="track.channels"> | {{ track.channels | channels }}</span> <span v-if="track.channels"> | {{ $filters.channels(track.channels) }}</span>
<span v-if="track.bitrate"> | {{ track.bitrate }} Kb/s</span> <span v-if="track.bitrate"> | {{ track.bitrate }} Kb/s</span>
</span> </span>
</p> </p>
<p> <p>
<span class="heading">Added at</span> <span class="heading">Added at</span>
<span class="title is-6">{{ track.time_added | time('L LT') }}</span> <span class="title is-6">{{ $filters.time(track.time_added, 'L LT') }}</span>
</p> </p>
<p> <p>
<span class="heading">Rating</span> <span class="heading">Rating</span>

View File

@ -6,7 +6,7 @@
close_action="Close" close_action="Close"
@ok="update_library" @ok="update_library"
@close="close()"> @close="close()">
<template slot="modal-content"> <template v-slot:modal-content>
<div v-if="!library.updating"> <div v-if="!library.updating">
<p class="mb-3">Scan for new, deleted and modified files</p> <p class="mb-3">Scan for new, deleted and modified files</p>
<div class="field" v-if="spotify_enabled || rss.tracks > 0"> <div class="field" v-if="spotify_enabled || rss.tracks > 0">
@ -36,7 +36,7 @@
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog' import ModalDialog from '@/components/ModalDialog.vue'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import webapi from '@/webapi' import webapi from '@/webapi'

View File

@ -53,14 +53,21 @@
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading">Volume</p> <p class="heading">Volume</p>
<range-slider <Slider v-model="player.volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
@change="set_volume"
:classes="{ target: 'slider'}" />
<!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
max="100" max="100"
step="1" step="1"
:value="player.volume" :value="player.volume"
@change="set_volume"> @change="set_volume">
</range-slider> </range-slider-->
</div> </div>
</div> </div>
</div> </div>
@ -82,7 +89,15 @@
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p> <p 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 <Slider v-model="stream_volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
:disabled="!playing"
@change="set_stream_volume"
:classes="{ target: 'slider'}" />
<!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
max="100" max="100"
@ -90,7 +105,7 @@
:disabled="!playing" :disabled="!playing"
:value="stream_volume" :value="stream_volume"
@change="set_stream_volume"> @change="set_stream_volume">
</range-slider> </range-slider-->
</div> </div>
</div> </div>
</div> </div>
@ -142,14 +157,21 @@
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading">Volume</p> <p class="heading">Volume</p>
<range-slider <Slider v-model="player.volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
@change="set_volume"
:classes="{ target: 'slider'}" />
<!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
max="100" max="100"
step="1" step="1"
:value="player.volume" :value="player.volume"
@change="set_volume"> @change="set_volume">
</range-slider> </range-slider-->
</div> </div>
</div> </div>
</div> </div>
@ -175,7 +197,15 @@
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p> <p 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 <Slider v-model="stream_volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
:disabled="!playing"
@change="set_stream_volume"
:classes="{ target: 'slider'}" />
<!-- range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
max="100" max="100"
@ -183,7 +213,7 @@
:disabled="!playing" :disabled="!playing"
:value="stream_volume" :value="stream_volume"
@change="set_stream_volume"> @change="set_stream_volume">
</range-slider> </range-slider-->
</div> </div>
</div> </div>
</div> </div>
@ -197,17 +227,18 @@
<script> <script>
import webapi from '@/webapi' import webapi from '@/webapi'
import _audio from '@/audio' import _audio from '@/audio'
import NavbarItemLink from './NavbarItemLink' import NavbarItemLink from './NavbarItemLink.vue'
import NavbarItemOutput from './NavbarItemOutput' import NavbarItemOutput from './NavbarItemOutput.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause' import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext' import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious' import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle' import PlayerButtonShuffle from '@/components/PlayerButtonShuffle.vue'
import PlayerButtonConsume from '@/components/PlayerButtonConsume' import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat' import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue'
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack' import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue'
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward' import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue'
import RangeSlider from 'vue-range-slider' //import RangeSlider from 'vue-range-slider'
import Slider from '@vueform/slider'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
export default { export default {
@ -215,7 +246,8 @@ export default {
components: { components: {
NavbarItemLink, NavbarItemLink,
NavbarItemOutput, NavbarItemOutput,
RangeSlider, //RangeSlider,
Slider,
PlayerButtonPlayPause, PlayerButtonPlayPause,
PlayerButtonNext, PlayerButtonNext,
PlayerButtonPrevious, PlayerButtonPrevious,

View File

@ -14,7 +14,15 @@
<div class="level-item fd-expanded"> <div class="level-item fd-expanded">
<div class="fd-expanded"> <div class="fd-expanded">
<p class="heading" :class="{ 'has-text-grey-light': !output.selected }">{{ output.name }}</p> <p class="heading" :class="{ 'has-text-grey-light': !output.selected }">{{ output.name }}</p>
<range-slider <Slider v-model="volume"
:min="0"
:max="100"
:step="1"
:tooltips="false"
:disabled="!output.selected"
@change="set_volume"
:classes="{ target: 'slider'}" />
<!--range-slider
class="slider fd-has-action" class="slider fd-has-action"
min="0" min="0"
max="100" max="100"
@ -22,7 +30,7 @@
:disabled="!output.selected" :disabled="!output.selected"
:value="volume" :value="volume"
@change="set_volume" > @change="set_volume" >
</range-slider> </range-slider-->
</div> </div>
</div> </div>
</div> </div>
@ -31,12 +39,16 @@
</template> </template>
<script> <script>
import RangeSlider from 'vue-range-slider' //import RangeSlider from 'vue-range-slider'
import Slider from '@vueform/slider'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'NavbarItemOutput', name: 'NavbarItemOutput',
components: { RangeSlider }, components: {
// RangeSlider
Slider
},
props: ['output'], props: ['output'],

View File

@ -79,7 +79,7 @@
</template> </template>
<script> <script>
import NavbarItemLink from './NavbarItemLink' import NavbarItemLink from './NavbarItemLink.vue'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
export default { export default {

View File

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

View File

@ -81,6 +81,7 @@ export default {
this.timerId = -1 this.timerId = -1
const newValue = this.$refs.settings_checkbox.checked const newValue = this.$refs.settings_checkbox.checked
console.log(this.$refs.settings_checkbox)
if (newValue === this.value) { if (newValue === this.value) {
this.statusUpdate = '' this.statusUpdate = ''
return return

View File

@ -1,14 +1,13 @@
<template functional> <template>
<div class="media"> <div class="media">
<div class="media-left fd-has-action" <div class="media-left fd-has-action"
v-if="$slots['artwork']" v-if="$slots['artwork']">
@click="listeners.click">
<slot name="artwork"></slot> <slot name="artwork"></slot>
</div> </div>
<div class="media-content fd-has-action is-clipped" @click="listeners.click"> <div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">{{ props.album.name }}</h1> <h1 class="title is-6">{{ album.name }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artists[0].name }}</b></h2> <h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2>
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ props.album.album_type }}, {{ props.album.release_date | time('L') }})</h2> <h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ $filters.time(album.release_date, 'L') }})</h2>
</div> </div>
<div class="media-right"> <div class="media-right">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -19,7 +19,7 @@
</p> </p>
<p> <p>
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date | time('L') }}</span> <span class="title is-6">{{ $filters.time(album.release_date, 'L') }}</span>
</p> </p>
<p> <p>
<span class="heading">Type</span> <span class="heading">Type</span>

View File

@ -23,7 +23,7 @@
</p> </p>
<p> <p>
<span class="heading">Release date</span> <span class="heading">Release date</span>
<span class="title is-6">{{ album.release_date | time('L') }}</span> <span class="title is-6">{{ $filters.time(album.release_date, 'L') }}</span>
</p> </p>
<p> <p>
<span class="heading">Track / Disc</span> <span class="heading">Track / Disc</span>
@ -31,7 +31,7 @@
</p> </p>
<p> <p>
<span class="heading">Length</span> <span class="heading">Length</span>
<span class="title is-6">{{ track.duration_ms | duration }}</span> <span class="title is-6">{{ $filters.duration(track.duration_ms) }}</span>
</p> </p>
<p> <p>
<span class="heading">Path</span> <span class="heading">Path</span>

View File

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

View File

@ -5,41 +5,53 @@
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="tabs is-centered is-small"> <div class="tabs is-centered is-small">
<ul> <ul>
<router-link tag="li" to="/music/browse" active-class="is-active"> <router-link to="/music/browse" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-web"></i></span> <span class="icon is-small"><i class="mdi mdi-web"></i></span>
<span class="">Browse</span> <span class="">Browse</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/music/artists" active-class="is-active"> <router-link to="/music/artists" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-artist"></i></span> <span class="icon is-small"><i class="mdi mdi-artist"></i></span>
<span class="">Artists</span> <span class="">Artists</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/music/albums" active-class="is-active"> <router-link to="/music/albums" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-album"></i></span> <span class="icon is-small"><i class="mdi mdi-album"></i></span>
<span class="">Albums</span> <span class="">Albums</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/music/genres" active-class="is-active"> <router-link to="/music/genres" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-speaker"></i></span> <span class="icon is-small"><i class="mdi mdi-speaker"></i></span>
<span class="">Genres</span> <span class="">Genres</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/music/composers" active-class="is-active"> <router-link to="/music/composers" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-book-open-page-variant"></i></span> <span class="icon is-small"><i class="mdi mdi-book-open-page-variant"></i></span>
<span class="">Composers</span> <span class="">Composers</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/music/spotify" v-if="spotify_enabled" active-class="is-active"> <router-link to="/music/spotify" v-if="spotify_enabled" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span> <span class="icon is-small"><i class="mdi mdi-spotify"></i></span>
<span class="">Spotify</span> <span class="">Spotify</span>
</a> </a>
</li>
</router-link> </router-link>
</ul> </ul>
</div> </div>

View File

@ -5,25 +5,33 @@
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="tabs is-centered is-small"> <div class="tabs is-centered is-small">
<ul> <ul>
<router-link tag="li" to="/settings/webinterface" active-class="is-active"> <router-link to="/settings/webinterface" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="">Webinterface</span> <span class="">Webinterface</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/settings/remotes-outputs" active-class="is-active"> <router-link to="/settings/remotes-outputs" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="">Remotes &amp; Outputs</span> <span class="">Remotes &amp; Outputs</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/settings/artwork" active-class="is-active"> <router-link to="/settings/artwork" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="">Artwork</span> <span class="">Artwork</span>
</a> </a>
</li>
</router-link> </router-link>
<router-link tag="li" to="/settings/online-services" active-class="is-active"> <router-link to="/settings/online-services" custom v-slot="{ navigate, isActive }">
<a> <li :class="{'is-active': isActive}">
<a @click="navigate" @keypress.enter="navigate">
<span class="">Online Services</span> <span class="">Online Services</span>
</a> </a>
</li>
</router-link> </router-link>
</ul> </ul>
</div> </div>

View File

@ -1,31 +1,32 @@
import Vue from 'vue'
import moment from 'moment' import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format' import momentDurationFormatSetup from 'moment-duration-format'
momentDurationFormatSetup(moment) momentDurationFormatSetup(moment)
Vue.filter('duration', function (value, format) {
export const filters = {
duration: function (value, format) {
if (format) { if (format) {
return moment.duration(value).format(format) return moment.duration(value).format(format)
} }
return moment.duration(value).format('hh:*mm:ss') return moment.duration(value).format('hh:*mm:ss')
}) },
Vue.filter('time', function (value, format) { time: function (value, format) {
if (format) { if (format) {
return moment(value).format(format) return moment(value).format(format)
} }
return moment(value).format() return moment(value).format()
}) },
Vue.filter('timeFromNow', function (value, withoutSuffix) { timeFromNow: function (value, withoutSuffix) {
return moment(value).fromNow(withoutSuffix) return moment(value).fromNow(withoutSuffix)
}) },
Vue.filter('number', function (value) { number: function (value) {
return value.toLocaleString() return value.toLocaleString()
}) },
Vue.filter('channels', function (value) { channels: function (value) {
if (value === 1) { if (value === 1) {
return 'mono' return 'mono'
} }
@ -36,4 +37,5 @@ Vue.filter('channels', function (value) {
return '' return ''
} }
return value + ' channels' return value + ' channels'
}) }
}

View File

@ -3,8 +3,31 @@
* Copyright (c) 2017 Adam Bender * Copyright (c) 2017 Adam Bender
* https://github.com/bendera/placeholder/blob/master/LICENSE * https://github.com/bendera/placeholder/blob/master/LICENSE
*/ */
class SVGRenderer {
render (data) { import stringToColor from 'string-to-color'
function is_background_light (background_color) {
// Based on https://stackoverflow.com/a/44615197
const hex = background_color.replace(/#/, '')
const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16)
const luma = [
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255
return luma > 0.5
}
function calc_text_color (background_color) {
return is_background_light(background_color) ? '#000000' : '#ffffff'
}
function createSVG (data) {
const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' + const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' +
'<defs>' + '<defs>' +
'<style type="text/css">' + '<style type="text/css">' +
@ -26,6 +49,21 @@ class SVGRenderer {
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg) return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
} }
function renderSVG (caption, alt_text, params) {
const background_color = stringToColor(alt_text)
const text_color = calc_text_color(background_color)
const paramsSVG = {
width: params.width,
height: params.height,
textColor: text_color,
backgroundColor: background_color,
caption: caption,
fontFamily: params.font_family,
fontSize: params.font_size,
fontWeight: params.font_weight
}
return createSVG(paramsSVG)
} }
export default SVGRenderer export { renderSVG }

31
web-src/src/main copy.js Normal file
View File

@ -0,0 +1,31 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import { router } from './router'
import store from './store'
import './filter'
import './progress'
import vClickOutside from 'v-click-outside'
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img'
import VueObserveVisibility from 'vue-observe-visibility'
import VueScrollTo from 'vue-scrollto'
import 'mdi/css/materialdesignicons.css'
import 'vue-range-slider/dist/vue-range-slider.css'
import './mystyles.scss'
Vue.config.productionTip = false
Vue.use(vClickOutside)
Vue.use(VueTinyLazyloadImg)
Vue.use(VueObserveVisibility)
Vue.use(VueScrollTo)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

View File

@ -1,31 +1,31 @@
// The Vue build version to load with the `import` command import { createApp } from 'vue'
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import { router } from './router'
import store from './store' import store from './store'
import './filter' import { router } from './router'
import './progress' import VueProgressBar from '@aacassandra/vue3-progressbar'
import vClickOutside from 'v-click-outside' import VueClickAway from "vue3-click-away"
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img' import VueLazyLoad from 'vue3-lazyload'
import VueObserveVisibility from 'vue-observe-visibility'
import VueScrollTo from 'vue-scrollto' import VueScrollTo from 'vue-scrollto'
import 'mdi/css/materialdesignicons.css' import { filters } from './filter'
import 'vue-range-slider/dist/vue-range-slider.css' import App from './App.vue'
import './mystyles.scss' import './mystyles.scss'
import 'mdi/css/materialdesignicons.css'
import '@vueform/slider/themes/default.css'
Vue.config.productionTip = false const app = createApp(App)
.use(store)
Vue.use(vClickOutside) .use(router)
Vue.use(VueTinyLazyloadImg) .use(VueProgressBar, {
Vue.use(VueObserveVisibility) color: 'hsl(204, 86%, 53%)',
Vue.use(VueScrollTo) failedColor: 'red',
height: '1px'
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
}) })
.use(VueClickAway)
.use(VueLazyLoad, {
// Do not log errors, if image does not exist
log: false
})
.use(VueScrollTo)
app.config.globalProperties.$filters = filters
app.mount('#app')

View File

@ -1,34 +1,47 @@
@charset "utf-8";
@import 'bulma'; @import 'bulma/bulma.sass';
@import '~bulma-switch'; @import 'bulma-switch';
/* Volume slider */
.slider { .slider {
min-width: 250px; min-width: 250px;
width: 100%; width: 100%;
} margin-top: 16px;
.range-slider-fill { margin-bottom: 16px;
background-color: hsl(0, 0%, 21%); --slider-height: 4px;
--slider-connect-bg: hsl(0, 0%, 21%);
--slider-tooltip-bg: hsl(0, 0%, 21%);
--slider-handle-ring-color: #3B82F630;
--slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.32);
--slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.42);
} }
.track-progress { /* Now playing progress bar */
margin: 0; .seek-slider {
padding: 0;
min-width: 250px; min-width: 250px;
width: 100%; max-width: 500px;
width: 100% !important;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
margin: 0 auto 16px auto;
--slider-height: 4px;
--slider-connect-bg: hsl(171, 100%, 41%);
--slider-tooltip-bg: hsl(171, 100%, 41%);
--slider-handle-bg: hsl(171, 100%, 41%);
--slider-handle-border: 0;
--slider-handle-width: 10px;
--slider-handle-height: 10px;
--slider-handle-radius: 9999px;
--slider-handle-shadow: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.32);
--slider-handle-shadow-active: 0.5px 0.5px 0.5px 0.5px rgba(0,0,0,.42);
--slider-handle-ring-width: 3px;
} }
.track-progress .range-slider-knob { .progress-bar {
visibility: hidden; background-color: $info;
} border-radius: 9999px;
height: 4px;
.track-progress .range-slider-fill {
background-color: hsl(217, 71%, 53%);
height: 2px;
}
.track-progress .range-slider-rail {
background-color: hsl(0, 0%, 100%);
} }
.media.with-progress h2:last-of-type { .media.with-progress h2:last-of-type {
@ -118,6 +131,14 @@ section.hero + 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); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
} }
.fd-page {
margin-top: 3.25rem;
}
.fd-page-with-tabs {
margin-top: 6.25rem !important;
}
/* Set minimum height to hide "option" section */ /* Set minimum height to hide "option" section */
.fd-content-with-option { .fd-content-with-option {
min-height: calc(100vh - 3.25rem - 3.25rem - 5rem); min-height: calc(100vh - 3.25rem - 3.25rem - 5rem);
@ -192,28 +213,17 @@ section.hero + section.fd-content {
} }
/* Transition effect */ /* Transition effect */
.fade-enter-active, .fade-leave-active { .fade-leave-active {
transition: opacity .4s; transition: opacity .2s ease;
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { .fade-enter-active {
transition: opacity .5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0; opacity: 0;
} }
.fade-enter-to, .fade-leave-from {
/* Now playing progress bar */ opacity: 1;
.seek-slider {
min-width: 250px;
max-width: 500px;
width: 100% !important;
}
.seek-slider .range-slider-fill {
background-color: hsl(171, 100%, 41%);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.seek-slider .range-slider-knob {
width: 10px;
height: 10px;
background-color: hsl(171, 100%, 41%);
border-color: hsl(171, 100%, 41%);
} }
/* Add a little bit of spacing between title and subtitle */ /* Add a little bit of spacing between title and subtitle */

View File

@ -34,27 +34,27 @@
<tbody> <tbody>
<tr> <tr>
<th>Artists</th> <th>Artists</th>
<td class="has-text-right">{{ library.artists | number }}</td> <td class="has-text-right">{{ $filters.number(library.artists) }}</td>
</tr> </tr>
<tr> <tr>
<th>Albums</th> <th>Albums</th>
<td class="has-text-right">{{ library.albums | number }}</td> <td class="has-text-right">{{ $filters.number(library.albums) }}</td>
</tr> </tr>
<tr> <tr>
<th>Tracks</th> <th>Tracks</th>
<td class="has-text-right">{{ library.songs | number }}</td> <td class="has-text-right">{{ $filters.number(library.songs) }}</td>
</tr> </tr>
<tr> <tr>
<th>Total playtime</th> <th>Total playtime</th>
<td class="has-text-right">{{ library.db_playtime * 1000 | duration('y [years], d [days], h [hours], m [minutes]') }}</td> <td class="has-text-right">{{ $filters.duration(library.db_playtime * 1000, 'y [years], d [days], h [hours], m [minutes]') }}</td>
</tr> </tr>
<tr> <tr>
<th>Library updated</th> <th>Library updated</th>
<td class="has-text-right">{{ library.updated_at | timeFromNow }} <span class="has-text-grey">({{ library.updated_at | time('lll') }})</span></td> <td class="has-text-right">{{ $filters.timeFromNow(library.updated_at) }} <span class="has-text-grey">({{ $filters.time(library.updated_at, 'lll') }})</span></td>
</tr> </tr>
<tr> <tr>
<th>Uptime</th> <th>Uptime</th>
<td class="has-text-right">{{ library.started_at | timeFromNow(true) }} <span class="has-text-grey">({{ library.started_at | time('ll') }})</span></td> <td class="has-text-right">{{ $filters.timeFromNow(library.started_at, true) }} <span class="has-text-grey">({{ $filters.time(library.started_at, 'll') }})</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -68,7 +68,7 @@
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
<div class="content has-text-centered-mobile"> <div class="content has-text-centered-mobile">
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p> <p class="is-size-7">Compiled with support for {{ config.buildoptions.join(', ') }}.</p>
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/owntone/owntone-server/network/dependencies">more</a>.</p> <p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/owntone/owntone-server/network/dependencies">more</a>.</p>
</div> </div>
</div> </div>
@ -107,12 +107,6 @@ export default {
showUpdateDialog () { showUpdateDialog () {
this.$store.commit(types.SHOW_UPDATE_DIALOG, true) this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
} }
},
filters: {
join: function (array) {
return array.join(', ')
}
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<content-with-hero> <content-with-hero>
<template slot="heading-left"> <template v-slot:heading-left>
<h1 class="title is-5">{{ album.name }}</h1> <h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2> <h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2>
@ -13,7 +13,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<p class="image is-square fd-has-shadow fd-has-action"> <p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
@ -22,7 +22,7 @@
@click="show_album_details_modal = true" /> @click="show_album_details_modal = true" />
</p> </p>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p> <p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks> <list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" /> <modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
@ -31,14 +31,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHero from '@/templates/ContentWithHero.vue'
import ContentWithHero from '@/templates/ContentWithHero' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_album(to.params.album_id), webapi.library_album(to.params.album_id),
@ -54,7 +53,6 @@ const albumData = {
export default { export default {
name: 'PageAlbum', name: 'PageAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
data () { data () {
@ -75,6 +73,19 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="albums_list.indexList"></index-button-list> <index-button-list :index="albums_list.indexList"></index-button-list>
<div class="columns"> <div class="columns">
@ -30,13 +30,13 @@
</div> </div>
</div> </div>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p> <p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list"></list-albums>
</template> </template>
</content-with-heading> </content-with-heading>
@ -44,17 +44,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums' import DropdownMenu from '@/components/DropdownMenu.vue'
import DropdownMenu from '@/components/DropdownMenu'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import Albums from '@/lib/Albums' import Albums from '@/lib/Albums'
const albumsData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_albums('music') return webapi.library_albums('music')
}, },
@ -69,7 +68,6 @@ const albumsData = {
export default { export default {
name: 'PageAlbums', name: 'PageAlbums',
mixins: [LoadDataBeforeEnterMixin(albumsData)],
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListAlbums, DropdownMenu }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListAlbums, DropdownMenu },
data () { data () {
@ -125,6 +123,23 @@ export default {
scrollToTop: function () { scrollToTop: function () {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
if (this.albums.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p class="heading" style="margin-bottom: 24px;">Sort by</p> <p class="heading" style="margin-bottom: 24px;">Sort by</p>
@ -8,10 +8,10 @@
</div> </div>
</div> </div>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">{{ artist.name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -21,7 +21,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p> <p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list"></list-albums>
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> <modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
@ -30,16 +30,15 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist' import DropdownMenu from '@/components/DropdownMenu.vue'
import DropdownMenu from '@/components/DropdownMenu'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import Albums from '@/lib/Albums' import Albums from '@/lib/Albums'
const artistData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_artist(to.params.artist_id), webapi.library_artist(to.params.artist_id),
@ -55,7 +54,6 @@ const artistData = {
export default { export default {
name: 'PageArtist', name: 'PageArtist',
mixins: [LoadDataBeforeEnterMixin(artistData)],
components: { ContentWithHeading, ListAlbums, ModalDialogArtist, DropdownMenu }, components: { ContentWithHeading, ListAlbums, ModalDialogArtist, DropdownMenu },
data () { data () {
@ -94,6 +92,19 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true) webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">{{ artist.name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -17,7 +17,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p> <p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p>
<list-tracks :tracks="tracks.items" :uris="track_uris"></list-tracks> <list-tracks :tracks="tracks.items" :uris="track_uris"></list-tracks>
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> <modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
@ -27,14 +27,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import webapi from '@/webapi' import webapi from '@/webapi'
const tracksData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_artist(to.params.artist_id), webapi.library_artist(to.params.artist_id),
@ -50,7 +49,6 @@ const tracksData = {
export default { export default {
name: 'PageArtistTracks', name: 'PageArtistTracks',
mixins: [LoadDataBeforeEnterMixin(tracksData)],
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist }, components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist },
data () { data () {
@ -82,6 +80,19 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true) webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="artists_list.indexList"></index-button-list> <index-button-list :index="artists_list.indexList"></index-button-list>
<div class="columns"> <div class="columns">
@ -30,13 +30,13 @@
</div> </div>
</div> </div>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Artists</p> <p class="heading">{{ artists_list.sortedAndFiltered.length }} Artists</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-artists :artists="artists_list"></list-artists> <list-artists :artists="artists_list"></list-artists>
</template> </template>
</content-with-heading> </content-with-heading>
@ -44,17 +44,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListArtists from '@/components/ListArtists.vue'
import ListArtists from '@/components/ListArtists' import DropdownMenu from '@/components/DropdownMenu.vue'
import DropdownMenu from '@/components/DropdownMenu'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import Artists from '@/lib/Artists' import Artists from '@/lib/Artists'
const artistsData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_artists('music') return webapi.library_artists('music')
}, },
@ -66,7 +65,6 @@ const artistsData = {
export default { export default {
name: 'PageArtists', name: 'PageArtists',
mixins: [LoadDataBeforeEnterMixin(artistsData)],
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu },
data () { data () {
@ -122,6 +120,23 @@ export default {
scrollToTop: function () { scrollToTop: function () {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
if (this.artists.items.length > 0) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<content-with-hero> <content-with-hero>
<template slot="heading-left"> <template v-slot:heading-left>
<h1 class="title is-5">{{ album.name }}</h1> <h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2> <h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2>
@ -13,7 +13,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<p class="image is-square fd-has-shadow fd-has-action"> <p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="album.artwork_url" :artwork_url="album.artwork_url"
@ -22,7 +22,7 @@
@click="show_album_details_modal = true" /> @click="show_album_details_modal = true" />
</p> </p>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p> <p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks> <list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" /> <modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" />
@ -31,14 +31,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHero from '@/templates/ContentWithHero.vue'
import ContentWithHero from '@/templates/ContentWithHero' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_album(to.params.album_id), webapi.library_album(to.params.album_id),
@ -54,7 +53,6 @@ const albumData = {
export default { export default {
name: 'PageAudiobooksAlbum', name: 'PageAudiobooksAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
data () { data () {
@ -84,6 +82,19 @@ export default {
this.selected_track = track this.selected_track = track
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,16 +1,16 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-audiobooks></tabs-audiobooks> <tabs-audiobooks></tabs-audiobooks>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="albums_list.indexList"></index-button-list> <index-button-list :index="albums_list.indexList"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Audiobooks</p> <p class="title is-4">Audiobooks</p>
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Audiobooks</p> <p class="heading">{{ albums_list.sortedAndFiltered.length }} Audiobooks</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list"></list-albums>
</template> </template>
</content-with-heading> </content-with-heading>
@ -18,15 +18,14 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import TabsAudiobooks from '@/components/TabsAudiobooks.vue'
import TabsAudiobooks from '@/components/TabsAudiobooks' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums'
import webapi from '@/webapi' import webapi from '@/webapi'
import Albums from '@/lib/Albums' import Albums from '@/lib/Albums'
const albumsData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_albums('audiobook') return webapi.library_albums('audiobook')
}, },
@ -38,7 +37,6 @@ const albumsData = {
export default { export default {
name: 'PageAudiobooksAlbums', name: 'PageAudiobooksAlbums',
mixins: [LoadDataBeforeEnterMixin(albumsData)],
components: { TabsAudiobooks, ContentWithHeading, IndexButtonList, ListAlbums }, components: { TabsAudiobooks, ContentWithHeading, IndexButtonList, ListAlbums },
data () { data () {
@ -57,6 +55,19 @@ export default {
}, },
methods: { methods: {
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">{{ artist.name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,7 +13,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums</p> <p class="heading has-text-centered-mobile">{{ artist.album_count }} albums</p>
<list-albums :albums="albums.items"></list-albums> <list-albums :albums="albums.items"></list-albums>
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> <modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
@ -22,13 +22,12 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import webapi from '@/webapi' import webapi from '@/webapi'
const artistData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_artist(to.params.artist_id), webapi.library_artist(to.params.artist_id),
@ -44,7 +43,6 @@ const artistData = {
export default { export default {
name: 'PageAudiobooksArtist', name: 'PageAudiobooksArtist',
mixins: [LoadDataBeforeEnterMixin(artistData)],
components: { ContentWithHeading, ListAlbums, ModalDialogArtist }, components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
data () { data () {
@ -60,6 +58,19 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), false) webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), false)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,18 +1,18 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-audiobooks></tabs-audiobooks> <tabs-audiobooks></tabs-audiobooks>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="artists_list.indexList"></index-button-list> <index-button-list :index="artists_list.indexList"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Authors</p> <p class="title is-4">Authors</p>
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Authors</p> <p class="heading">{{ artists_list.sortedAndFiltered.length }} Authors</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-artists :artists="artists_list"></list-artists> <list-artists :artists="artists_list"></list-artists>
</template> </template>
</content-with-heading> </content-with-heading>
@ -20,15 +20,14 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsAudiobooks from '@/components/TabsAudiobooks.vue'
import TabsAudiobooks from '@/components/TabsAudiobooks' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListArtists from '@/components/ListArtists.vue'
import ListArtists from '@/components/ListArtists'
import webapi from '@/webapi' import webapi from '@/webapi'
import Artists from '@/lib/Artists' import Artists from '@/lib/Artists'
const artistsData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_artists('audiobook') return webapi.library_artists('audiobook')
}, },
@ -40,7 +39,6 @@ const artistsData = {
export default { export default {
name: 'PageAudiobooksArtists', name: 'PageAudiobooksArtists',
mixins: [LoadDataBeforeEnterMixin(artistsData)],
components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists }, components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists },
data () { data () {
@ -59,6 +57,19 @@ export default {
}, },
methods: { methods: {
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

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

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Recently added</p> <p class="title is-4">Recently added</p>
<p class="heading">albums</p> <p class="heading">albums</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="albums_list"></list-albums> <list-albums :albums="albums_list"></list-albums>
</template> </template>
</content-with-heading> </content-with-heading>
@ -15,15 +15,14 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums'
import webapi from '@/webapi' import webapi from '@/webapi'
import store from '@/store' import store from '@/store'
import Albums from '@/lib/Albums' import Albums from '@/lib/Albums'
const browseData = { const dataObject = {
load: function (to) { load: function (to) {
const limit = store.getters.settings_option_recently_added_limit const limit = store.getters.settings_option_recently_added_limit
return webapi.search({ return webapi.search({
@ -40,7 +39,6 @@ const browseData = {
export default { export default {
name: 'PageBrowseType', name: 'PageBrowseType',
mixins: [LoadDataBeforeEnterMixin(browseData)],
components: { ContentWithHeading, TabsMusic, ListAlbums }, components: { ContentWithHeading, TabsMusic, ListAlbums },
data () { data () {
@ -58,6 +56,19 @@ export default {
group: true group: true
}) })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Recently played</p> <p class="title is-4">Recently played</p>
<p class="heading">tracks</p> <p class="heading">tracks</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-tracks :tracks="recently_played.items"></list-tracks> <list-tracks :tracks="recently_played.items"></list-tracks>
</template> </template>
</content-with-heading> </content-with-heading>
@ -15,13 +15,12 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks'
import webapi from '@/webapi' import webapi from '@/webapi'
const browseData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.search({ return webapi.search({
type: 'track', type: 'track',
@ -37,13 +36,25 @@ const browseData = {
export default { export default {
name: 'PageBrowseType', name: 'PageBrowseType',
mixins: [LoadDataBeforeEnterMixin(browseData)],
components: { ContentWithHeading, TabsMusic, ListTracks }, components: { ContentWithHeading, TabsMusic, ListTracks },
data () { data () {
return { return {
recently_played: {} recently_played: {}
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,10 +1,13 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
</template>
<template v-slot:heading-left>
<p class="title is-4">{{ name }}</p> <p class="title is-4">{{ name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -14,10 +17,10 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ composer_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p> <p class="heading has-text-centered-mobile">{{ composer_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p>
<list-item-albums v-for="album in composer_albums.items" :key="album.id" :album="album" @click="open_album(album)"> <list-item-albums v-for="album in composer_albums.items" :key="album.id" :album="album" @click="open_album(album)">
<template slot="actions"> <template slot:actions>
<a @click="open_dialog(album)"> <a @click="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -31,14 +34,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListItemAlbums from '@/components/ListItemAlbum.vue'
import ListItemAlbums from '@/components/ListItemAlbum' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum' import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer'
import webapi from '@/webapi' import webapi from '@/webapi'
const composerData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_composer(to.params.composer) return webapi.library_composer(to.params.composer)
}, },
@ -51,7 +53,6 @@ const composerData = {
export default { export default {
name: 'PageComposer', name: 'PageComposer',
mixins: [LoadDataBeforeEnterMixin(composerData)],
components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer }, components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer },
data () { data () {
@ -90,6 +91,19 @@ export default {
this.selected_album = album this.selected_album = album
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,10 +1,13 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
</template>
<template v-slot:heading-left>
<p class="title is-4">{{ composer }}</p> <p class="title is-4">{{ composer }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -14,11 +17,11 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_albums">albums</a> | {{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_albums">albums</a> | {{ tracks.total }} tracks</p>
<list-item-track v-for="(track, index) in rated_tracks" :key="track.id" :track="track" @click="play_track(index)"> <list-item-track v-for="(track, index) in rated_tracks" :key="track.id" :track="track" @click="play_track(index)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -31,14 +34,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListItemTrack from '@/components/ListItemTrack.vue'
import ListItemTrack from '@/components/ListItemTrack' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack' import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer'
import webapi from '@/webapi' import webapi from '@/webapi'
const tracksData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_composer_tracks(to.params.composer) return webapi.library_composer_tracks(to.params.composer)
}, },
@ -51,7 +53,6 @@ const tracksData = {
export default { export default {
name: 'PageComposerTracks', name: 'PageComposerTracks',
mixins: [LoadDataBeforeEnterMixin(tracksData)],
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogComposer }, components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogComposer },
data () { data () {
@ -104,6 +105,19 @@ export default {
this.selected_track = track this.selected_track = track
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -3,14 +3,14 @@
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="composers_list.indexList"></index-button-list> <index-button-list :index="composers_list.indexList"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ heading }}</p> <p class="title is-4">{{ heading }}</p>
<p class="heading">{{ composers.total }} composers</p> <p class="heading">{{ composers.total }} composers</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-composers :composers="composers_list"></list-composers> <list-composers :composers="composers_list"></list-composers>
</template> </template>
</content-with-heading> </content-with-heading>
@ -18,15 +18,14 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListComposers from '@/components/ListComposers.vue'
import ListComposers from '@/components/ListComposers'
import webapi from '@/webapi' import webapi from '@/webapi'
import Composers from '@/lib/Composers' import Composers from '@/lib/Composers'
const composersData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_composers() return webapi.library_composers()
}, },
@ -44,7 +43,6 @@ const composersData = {
export default { export default {
name: 'PageComposers', name: 'PageComposers',
mixins: [LoadDataBeforeEnterMixin(composersData)],
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListComposers },
data () { data () {
@ -80,6 +78,19 @@ export default {
this.selected_composer = composer this.selected_composer = composer
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Files</p> <p class="title is-4">Files</p>
<p class="title is-7 has-text-grey">{{ current_directory }}</p> <p class="title is-7 has-text-grey">{{ current_directory }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="open_directory_dialog({ 'path': current_directory })"> <a class="button is-small is-light is-rounded" @click="open_directory_dialog({ 'path': current_directory })">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -15,7 +15,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<div class="media" v-if="$route.query.directory" @click="open_parent_directory()"> <div class="media" v-if="$route.query.directory" @click="open_parent_directory()">
<figure class="media-left fd-has-action"> <figure class="media-left fd-has-action">
<span class="icon"> <span class="icon">
@ -31,7 +31,7 @@
</div> </div>
<list-item-directory v-for="directory in files.directories" :key="directory.path" :directory="directory" @click="open_directory(directory)"> <list-item-directory v-for="directory in files.directories" :key="directory.path" :directory="directory" @click="open_directory(directory)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_directory_dialog(directory)"> <a @click="open_directory_dialog(directory)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -39,12 +39,12 @@
</list-item-directory> </list-item-directory>
<list-item-playlist v-for="playlist in files.playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)"> <list-item-playlist v-for="playlist in files.playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template slot="icon"> <template v-slot:icon>
<span class="icon"> <span class="icon">
<i class="mdi mdi-library-music"></i> <i class="mdi mdi-library-music"></i>
</span> </span>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_playlist_dialog(playlist)"> <a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -52,12 +52,12 @@
</list-item-playlist> </list-item-playlist>
<list-item-track v-for="(track, index) in files.tracks.items" :key="track.id" :track="track" @click="play_track(index)"> <list-item-track v-for="(track, index) in files.tracks.items" :key="track.id" :track="track" @click="play_track(index)">
<template slot="icon"> <template v-slot:icon>
<span class="icon"> <span class="icon">
<i class="mdi mdi-file-outline"></i> <i class="mdi mdi-file-outline"></i>
</span> </span>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -73,17 +73,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListItemDirectory from '@/components/ListItemDirectory.vue'
import ListItemDirectory from '@/components/ListItemDirectory' import ListItemPlaylist from '@/components/ListItemPlaylist.vue'
import ListItemPlaylist from '@/components/ListItemPlaylist' import ListItemTrack from '@/components/ListItemTrack.vue'
import ListItemTrack from '@/components/ListItemTrack' import ModalDialogDirectory from '@/components/ModalDialogDirectory.vue'
import ModalDialogDirectory from '@/components/ModalDialogDirectory' import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi' import webapi from '@/webapi'
const filesData = { const dataObject = {
load: function (to) { load: function (to) {
if (to.query.directory) { if (to.query.directory) {
return webapi.library_files(to.query.directory) return webapi.library_files(to.query.directory)
@ -106,7 +105,6 @@ const filesData = {
export default { export default {
name: 'PageFiles', name: 'PageFiles',
mixins: [LoadDataBeforeEnterMixin(filesData)],
components: { ContentWithHeading, ListItemDirectory, ListItemPlaylist, ListItemTrack, ModalDialogDirectory, ModalDialogPlaylist, ModalDialogTrack }, components: { ContentWithHeading, ListItemDirectory, ListItemPlaylist, ListItemTrack, ModalDialogDirectory, ModalDialogPlaylist, ModalDialogTrack },
data () { data () {
@ -173,6 +171,19 @@ export default {
this.selected_playlist = playlist this.selected_playlist = playlist
this.show_playlist_details_modal = true this.show_playlist_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ name }}</p> <p class="title is-4">{{ name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -17,7 +17,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p> <p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p>
<list-albums :albums="genre_albums.items"></list-albums> <list-albums :albums="genre_albums.items"></list-albums>
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" /> <modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" />
@ -27,14 +27,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums' import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre'
import webapi from '@/webapi' import webapi from '@/webapi'
const genreData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_genre(to.params.genre) return webapi.library_genre(to.params.genre)
}, },
@ -47,7 +46,6 @@ const genreData = {
export default { export default {
name: 'PageGenre', name: 'PageGenre',
mixins: [LoadDataBeforeEnterMixin(genreData)],
components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre }, components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre },
data () { data () {
@ -80,6 +78,19 @@ export default {
this.selected_album = album this.selected_album = album
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ genre }}</p> <p class="title is-4">{{ genre }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_genre_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -17,7 +17,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p>
<list-tracks :tracks="tracks.items" :expression="expression"></list-tracks> <list-tracks :tracks="tracks.items" :expression="expression"></list-tracks>
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" /> <modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" />
@ -27,14 +27,13 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks' import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre'
import webapi from '@/webapi' import webapi from '@/webapi'
const tracksData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_genre_tracks(to.params.genre) return webapi.library_genre_tracks(to.params.genre)
}, },
@ -47,7 +46,6 @@ const tracksData = {
export default { export default {
name: 'PageGenreTracks', name: 'PageGenreTracks',
mixins: [LoadDataBeforeEnterMixin(tracksData)],
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre }, components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre },
data () { data () {
@ -79,6 +77,19 @@ export default {
play: function () { play: function () {
webapi.player_play_expression(this.expression, true) webapi.player_play_expression(this.expression, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,19 +1,19 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="options"> <template v-slot:options>
<index-button-list :index="index_list"></index-button-list> <index-button-list :index="index_list"></index-button-list>
</template> </template>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Genres</p> <p class="title is-4">Genres</p>
<p class="heading">{{ genres.total }} genres</p> <p class="heading">{{ genres.total }} genres</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre" @click="open_genre(genre)"> <list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre" @click="open_genre(genre)">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(genre)"> <a @click.prevent.stop="open_dialog(genre)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -25,15 +25,14 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import IndexButtonList from '@/components/IndexButtonList.vue'
import IndexButtonList from '@/components/IndexButtonList' import ListItemGenre from '@/components/ListItemGenre.vue'
import ListItemGenre from '@/components/ListItemGenre' import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre'
import webapi from '@/webapi' import webapi from '@/webapi'
const genresData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_genres() return webapi.library_genres()
}, },
@ -45,7 +44,6 @@ const genresData = {
export default { export default {
name: 'PageGenres', name: 'PageGenres',
mixins: [LoadDataBeforeEnterMixin(genresData)],
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre }, components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre, ModalDialogGenre },
data () { data () {
@ -73,6 +71,19 @@ export default {
this.selected_genre = genre this.selected_genre = genre
this.show_details_modal = true this.show_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -11,7 +11,15 @@
<div class="fd-has-padding-left-right"> <div class="fd-has-padding-left-right">
<div class="container has-text-centered"> <div class="container has-text-centered">
<p class="control has-text-centered fd-progress-now-playing"> <p class="control has-text-centered fd-progress-now-playing">
<range-slider <Slider v-model="item_progress_ms"
:min="0"
:max="state.item_length_ms"
:step="1000"
:tooltips="false"
:disabled="state.state === 'stop'"
@change="seek"
:classes="{ target: 'seek-slider'}" />
<!--range-slider
class="seek-slider fd-has-action" class="seek-slider fd-has-action"
min="0" min="0"
:max="state.item_length_ms" :max="state.item_length_ms"
@ -19,10 +27,10 @@
:disabled="state.state === 'stop'" :disabled="state.state === 'stop'"
step="1000" step="1000"
@change="seek" > @change="seek" >
</range-slider> </range-slider-->
</p> </p>
<p class="content"> <p class="content">
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span> <span>{{ $filters.duration(item_progress_ms) }} / {{ $filters.duration(now_playing.length_ms) }}</span>
</p> </p>
</div> </div>
</div> </div>
@ -60,15 +68,21 @@
</template> </template>
<script> <script>
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem' import ModalDialogQueueItem from '@/components/ModalDialogQueueItem.vue'
import RangeSlider from 'vue-range-slider' //import RangeSlider from 'vue-range-slider'
import CoverArtwork from '@/components/CoverArtwork' import Slider from '@vueform/slider'
import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
export default { export default {
name: 'PageNowPlaying', name: 'PageNowPlaying',
components: { ModalDialogQueueItem, RangeSlider, CoverArtwork }, components: {
ModalDialogQueueItem,
// RangeSlider,
Slider,
CoverArtwork
},
data () { data () {
return { return {

View File

@ -1,9 +1,9 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">{{ playlist.name }}</div> <div class="title is-4">{{ playlist.name }}</div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,7 +13,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p> <p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
<list-tracks :tracks="tracks" :uris="uris"></list-tracks> <list-tracks :tracks="tracks" :uris="uris"></list-tracks>
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" :uris="uris" @close="show_playlist_details_modal = false" /> <modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" :uris="uris" @close="show_playlist_details_modal = false" />
@ -22,13 +22,12 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks' import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
import webapi from '@/webapi' import webapi from '@/webapi'
const playlistData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_playlist(to.params.playlist_id), webapi.library_playlist(to.params.playlist_id),
@ -44,7 +43,6 @@ const playlistData = {
export default { export default {
name: 'PagePlaylist', name: 'PagePlaylist',
mixins: [LoadDataBeforeEnterMixin(playlistData)],
components: { ContentWithHeading, ListTracks, ModalDialogPlaylist }, components: { ContentWithHeading, ListTracks, ModalDialogPlaylist },
data () { data () {
@ -69,6 +67,19 @@ export default {
play: function () { play: function () {
webapi.player_play_uri(this.uris, true) webapi.player_play_uri(this.uris, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,22 +1,21 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ playlist.name }}</p> <p class="title is-4">{{ playlist.name }}</p>
<p class="heading">{{ playlists.total }} playlists</p> <p class="heading">{{ playlists.total }} playlists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-playlists :playlists="playlists.items"></list-playlists> <list-playlists :playlists="playlists.items"></list-playlists>
</template> </template>
</content-with-heading> </content-with-heading>
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListPlaylists from '@/components/ListPlaylists.vue'
import ListPlaylists from '@/components/ListPlaylists'
import webapi from '@/webapi' import webapi from '@/webapi'
const playlistsData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_playlist(to.params.playlist_id), webapi.library_playlist(to.params.playlist_id),
@ -32,7 +31,6 @@ const playlistsData = {
export default { export default {
name: 'PagePlaylists', name: 'PagePlaylists',
mixins: [LoadDataBeforeEnterMixin(playlistsData)],
components: { ContentWithHeading, ListPlaylists }, components: { ContentWithHeading, ListPlaylists },
data () { data () {
@ -40,6 +38,19 @@ export default {
playlist: {}, playlist: {},
playlists: {} playlists: {}
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,10 +1,10 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">{{ album.name }} <div class="title is-4">{{ album.name }}
</div> </div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -17,21 +17,14 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p> <p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
<list-item-track v-for="track in tracks" :key="track.id" :track="track" @click="play_track(track)"> <list-item-track v-for="track in tracks" :key="track.id" :track="track" @click="play_track(track)">
<template slot="progress"> <template v-slot:progress>
<range-slider <progress-bar :max="track.length_ms" :value="track.seek_ms" />
class="track-progress"
min="0"
:max="track.length_ms"
step="1"
:disabled="true"
:value="track.seek_ms" >
</range-slider>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(track)"> <a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
@ -55,7 +48,7 @@
delete_action="Remove" delete_action="Remove"
@close="show_remove_podcast_modal = false" @close="show_remove_podcast_modal = false"
@delete="remove_podcast"> @delete="remove_podcast">
<template slot="modal-content"> <template v-slot:modal-content>
<p>Permanently remove this podcast from your library?</p> <p>Permanently remove this podcast from your library?</p>
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p> <p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p>
</template> </template>
@ -65,16 +58,15 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListItemTrack from '@/components/ListItemTrack.vue'
import ListItemTrack from '@/components/ListItemTrack' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum' import ModalDialog from '@/components/ModalDialog.vue'
import ModalDialog from '@/components/ModalDialog' import ProgressBar from '@/components/ProgressBar.vue'
import RangeSlider from 'vue-range-slider'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_album(to.params.album_id), webapi.library_album(to.params.album_id),
@ -90,8 +82,14 @@ const albumData = {
export default { export default {
name: 'PagePodcast', name: 'PagePodcast',
mixins: [LoadDataBeforeEnterMixin(albumData)], components: {
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, RangeSlider, ModalDialogAlbum, ModalDialog }, ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ModalDialogAlbum,
ModalDialog,
ProgressBar
},
data () { data () {
return { return {
@ -154,6 +152,19 @@ export default {
this.tracks = data.tracks.items this.tracks = data.tracks.items
}) })
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<content-with-heading v-if="new_episodes.items.length > 0"> <content-with-heading v-if="new_episodes.items.length > 0">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">New episodes</p> <p class="title is-4">New episodes</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small" @click="mark_all_played"> <a class="button is-small" @click="mark_all_played">
<span class="icon"> <span class="icon">
@ -14,19 +14,12 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-item-track v-for="track in new_episodes.items" :key="track.id" :track="track" @click="play_track(track)"> <list-item-track v-for="track in new_episodes.items" :key="track.id" :track="track" @click="play_track(track)">
<template slot="progress"> <template v-slot:progress>
<range-slider <progress-bar :max="track.length_ms" :value="track.seek_ms" />
class="track-progress"
min="0"
:max="track.length_ms"
step="1"
:disabled="true"
:value="track.seek_ms" >
</range-slider>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -37,11 +30,11 @@
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Podcasts</p> <p class="title is-4">Podcasts</p>
<p class="heading">{{ albums.total }} podcasts</p> <p class="heading">{{ albums.total }} podcasts</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a v-if="rss.tracks > 0" class="button is-small" @click="update_rss"> <a v-if="rss.tracks > 0" class="button is-small" @click="update_rss">
<span class="icon"> <span class="icon">
@ -57,7 +50,7 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="albums.items" <list-albums :albums="albums.items"
@play-count-changed="reload_new_episodes()" @play-count-changed="reload_new_episodes()"
@podcast-deleted="reload_podcasts()"> @podcast-deleted="reload_podcasts()">
@ -72,17 +65,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListItemTrack from '@/components/ListItemTrack.vue'
import ListItemTrack from '@/components/ListItemTrack' import ListAlbums from '@/components/ListAlbums.vue'
import ListAlbums from '@/components/ListAlbums' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack' import ModalDialogAddRss from '@/components/ModalDialogAddRss.vue'
import ModalDialogAddRss from '@/components/ModalDialogAddRss' import ProgressBar from '@/components/ProgressBar.vue'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import RangeSlider from 'vue-range-slider'
import webapi from '@/webapi' import webapi from '@/webapi'
const albumsData = { const dataObject = {
load: function (to) { load: function (to) {
return Promise.all([ return Promise.all([
webapi.library_albums('podcast'), webapi.library_albums('podcast'),
@ -98,8 +90,14 @@ const albumsData = {
export default { export default {
name: 'PagePodcasts', name: 'PagePodcasts',
mixins: [LoadDataBeforeEnterMixin(albumsData)], components: {
components: { ContentWithHeading, ListItemTrack, ListAlbums, ModalDialogTrack, ModalDialogAddRss, RangeSlider }, ContentWithHeading,
ListItemTrack,
ListAlbums,
ModalDialogTrack,
ModalDialogAddRss,
ProgressBar
},
data () { data () {
return { return {
@ -157,6 +155,19 @@ export default {
this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, 'rss') this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, 'rss')
this.$store.commit(types.SHOW_UPDATE_DIALOG, true) this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,10 +1,10 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="heading">{{ queue.count }} tracks</p> <p class="heading">{{ queue.count }} tracks</p>
<p class="title is-4">Queue</p> <p class="title is-4">Queue</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small" :class="{ 'is-info': show_only_next_items }" @click="update_show_next_items"> <a class="button is-small" :class="{ 'is-info': show_only_next_items }" @click="update_show_next_items">
<span class="icon"> <span class="icon">
@ -38,22 +38,25 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<draggable v-model="queue_items" handle=".handle" @end="move_item"> <draggable v-model="queue_items" handle=".handle" item-key="id" @end="move_item">
<list-item-queue-item v-for="(item, index) in queue_items" <template #item="{ element, index }">
:key="item.id" :item="item" :position="index" <list-item-queue-item
:item="element"
:position="index"
:current_position="current_position" :current_position="current_position"
:show_only_next_items="show_only_next_items" :show_only_next_items="show_only_next_items"
:edit_mode="edit_mode"> :edit_mode="edit_mode">
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(item)" v-if="!edit_mode"> <a @click.prevent.stop="open_dialog(element)" v-if="!edit_mode">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
<a @click="remove(item)" v-if="item.id !== state.item_id && edit_mode"> <a @click.prevent.stop="remove(element)" v-if="element.id !== state.item_id && edit_mode">
<span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span> <span class="icon has-text-grey"><i class="mdi mdi-delete mdi-18px"></i></span>
</a> </a>
</template> </template>
</list-item-queue-item> </list-item-queue-item>
</template>
</draggable> </draggable>
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" /> <modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
<modal-dialog-add-url-stream :show="show_url_modal" @close="show_url_modal = false" /> <modal-dialog-add-url-stream :show="show_url_modal" @close="show_url_modal = false" />
@ -63,11 +66,11 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemQueueItem from '@/components/ListItemQueueItem' import ListItemQueueItem from '@/components/ListItemQueueItem.vue'
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem' import ModalDialogQueueItem from '@/components/ModalDialogQueueItem.vue'
import ModalDialogAddUrlStream from '@/components/ModalDialogAddUrlStream' import ModalDialogAddUrlStream from '@/components/ModalDialogAddUrlStream.vue'
import ModalDialogPlaylistSave from '@/components/ModalDialogPlaylistSave' import ModalDialogPlaylistSave from '@/components/ModalDialogPlaylistSave.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Radio</p> <p class="title is-4">Radio</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p>
<list-tracks :tracks="tracks.items"></list-tracks> <list-tracks :tracks="tracks.items"></list-tracks>
</template> </template>
@ -13,12 +13,11 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import ListTracks from '@/components/ListTracks.vue'
import ListTracks from '@/components/ListTracks'
import webapi from '@/webapi' import webapi from '@/webapi'
const streamsData = { const dataObject = {
load: function (to) { load: function (to) {
return webapi.library_radio_streams() return webapi.library_radio_streams()
}, },
@ -30,13 +29,25 @@ const streamsData = {
export default { export default {
name: 'PageRadioStreams', name: 'PageRadioStreams',
mixins: [LoadDataBeforeEnterMixin(streamsData)],
components: { ContentWithHeading, ListTracks }, components: { ContentWithHeading, ListTracks },
data () { data () {
return { return {
tracks: { items: [] } tracks: { items: [] }
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -30,13 +30,13 @@
<!-- Tracks --> <!-- Tracks -->
<content-with-heading v-if="show_tracks && tracks.total"> <content-with-heading v-if="show_tracks && tracks.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Tracks</p> <p class="title is-4">Tracks</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-tracks :tracks="tracks.items"></list-tracks> <list-tracks :tracks="tracks.items"></list-tracks>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_tracks_button" class="level"> <nav v-if="show_all_tracks_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a>
@ -45,20 +45,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_tracks && !tracks.total" class="mt-6"> <content-text v-if="show_tracks && !tracks.total" class="mt-6">
<template slot="content"> <template v-slot:content>
<p><i>No tracks found</i></p> <p><i>No tracks found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Artists --> <!-- Artists -->
<content-with-heading v-if="show_artists && artists.total"> <content-with-heading v-if="show_artists && artists.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-artists :artists="artists.items"></list-artists> <list-artists :artists="artists.items"></list-artists>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_artists_button" class="level"> <nav v-if="show_all_artists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a>
@ -67,20 +67,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_artists && !artists.total"> <content-text v-if="show_artists && !artists.total">
<template slot="content"> <template v-slot:content>
<p><i>No artists found</i></p> <p><i>No artists found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Albums --> <!-- Albums -->
<content-with-heading v-if="show_albums && albums.total"> <content-with-heading v-if="show_albums && albums.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="albums.items"></list-albums> <list-albums :albums="albums.items"></list-albums>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_albums_button" class="level"> <nav v-if="show_all_albums_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a>
@ -89,20 +89,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_albums && !albums.total"> <content-text v-if="show_albums && !albums.total">
<template slot="content"> <template v-slot:content>
<p><i>No albums found</i></p> <p><i>No albums found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Composers --> <!-- Composers -->
<content-with-heading v-if="show_composers && composers.total"> <content-with-heading v-if="show_composers && composers.total">
<template slot="heading-left"> <template slot:heading-left>
<p class="title is-4">Composers</p> <p class="title is-4">Composers</p>
</template> </template>
<template slot="content"> <template slot:content>
<list-composers :composers="composers.items"></list-composers> <list-composers :composers="composers.items"></list-composers>
</template> </template>
<template slot="footer"> <template slot:footer>
<nav v-if="show_all_composers_button" class="level"> <nav v-if="show_all_composers_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_composers">Show all {{ composers.total }} composers</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_composers">Show all {{ composers.total }} composers</a>
@ -111,20 +111,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_composers && !composers.total"> <content-text v-if="show_composers && !composers.total">
<template slot="content"> <template slot:content>
<p><i>No composers found</i></p> <p><i>No composers found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Playlists --> <!-- Playlists -->
<content-with-heading v-if="show_playlists && playlists.total"> <content-with-heading v-if="show_playlists && playlists.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Playlists</p> <p class="title is-4">Playlists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-playlists :playlists="playlists.items"></list-playlists> <list-playlists :playlists="playlists.items"></list-playlists>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_playlists_button" class="level"> <nav v-if="show_all_playlists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a>
@ -133,20 +133,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_playlists && !playlists.total"> <content-text v-if="show_playlists && !playlists.total">
<template slot="content"> <template v-slot:content>
<p><i>No playlists found</i></p> <p><i>No playlists found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Podcasts --> <!-- Podcasts -->
<content-with-heading v-if="show_podcasts && podcasts.total"> <content-with-heading v-if="show_podcasts && podcasts.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Podcasts</p> <p class="title is-4">Podcasts</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="podcasts.items"></list-albums> <list-albums :albums="podcasts.items"></list-albums>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_podcasts_button" class="level"> <nav v-if="show_all_podcasts_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_podcasts">Show all {{ podcasts.total.toLocaleString() }} podcasts</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_podcasts">Show all {{ podcasts.total.toLocaleString() }} podcasts</a>
@ -155,20 +155,20 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_podcasts && !podcasts.total"> <content-text v-if="show_podcasts && !podcasts.total">
<template slot="content"> <template v-slot:content>
<p><i>No podcasts found</i></p> <p><i>No podcasts found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Audiobooks --> <!-- Audiobooks -->
<content-with-heading v-if="show_audiobooks && audiobooks.total"> <content-with-heading v-if="show_audiobooks && audiobooks.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Audiobooks</p> <p class="title is-4">Audiobooks</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<list-albums :albums="audiobooks.items"></list-albums> <list-albums :albums="audiobooks.items"></list-albums>
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_audiobooks_button" class="level"> <nav v-if="show_all_audiobooks_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_audiobooks">Show all {{ audiobooks.total.toLocaleString() }} audiobooks</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_audiobooks">Show all {{ audiobooks.total.toLocaleString() }} audiobooks</a>
@ -177,7 +177,7 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_audiobooks && !audiobooks.total"> <content-text v-if="show_audiobooks && !audiobooks.total">
<template slot="content"> <template v-slot:content>
<p><i>No audiobooks found</i></p> <p><i>No audiobooks found</i></p>
</template> </template>
</content-text> </content-text>
@ -185,14 +185,14 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentText from '@/templates/ContentText' import ContentText from '@/templates/ContentText.vue'
import TabsSearch from '@/components/TabsSearch' import TabsSearch from '@/components/TabsSearch.vue'
import ListTracks from '@/components/ListTracks' import ListTracks from '@/components/ListTracks.vue'
import ListArtists from '@/components/ListArtists' import ListArtists from '@/components/ListArtists.vue'
import ListAlbums from '@/components/ListAlbums' import ListAlbums from '@/components/ListAlbums.vue'
import ListComposers from '@/components/ListComposers' import ListComposers from '@/components/ListComposers.vue'
import ListPlaylists from '@/components/ListPlaylists' import ListPlaylists from '@/components/ListPlaylists.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-settings></tabs-settings> <tabs-settings></tabs-settings>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Artwork</div> <div class="title is-4">Artwork</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<div class="content"> <div class="content">
<p> <p>
OwnTone supports PNG and JPEG artwork which is either placed as separate image files in the library, OwnTone supports PNG and JPEG artwork which is either placed as separate image files in the library,
@ -16,13 +16,13 @@
<p>In addition to that, you can enable fetching artwork from the following artwork providers:</p> <p>In addition to that, you can enable fetching artwork from the following artwork providers:</p>
</div> </div>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_spotify" v-if="spotify.libspotify_logged_in"> <settings-checkbox category_name="artwork" option_name="use_artwork_source_spotify" v-if="spotify.libspotify_logged_in">
<template slot="label"> Spotify</template> <template v-slot:label> Spotify</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_discogs"> <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> <template v-slot:label> Discogs (<a href="https://www.discogs.com/">https://www.discogs.com/</a>)</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="artwork" option_name="use_artwork_source_coverartarchive"> <settings-checkbox 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> <template v-slot:label> Cover Art Archive (<a href="https://coverartarchive.org/">https://coverartarchive.org/</a>)</template>
</settings-checkbox> </settings-checkbox>
</template> </template>
</content-with-heading> </content-with-heading>
@ -30,9 +30,9 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings' import TabsSettings from '@/components/TabsSettings.vue'
import SettingsCheckbox from '@/components/SettingsCheckbox' import SettingsCheckbox from '@/components/SettingsCheckbox.vue'
export default { export default {
name: 'SettingsPageArtwork', name: 'SettingsPageArtwork',

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-settings></tabs-settings> <tabs-settings></tabs-settings>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Spotify</div> <div class="title is-4">Spotify</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<div class="notification is-size-7" v-if="!spotify.spotify_installed"> <div class="notification is-size-7" v-if="!spotify.spotify_installed">
<p>OwnTone was either built without support for Spotify or libspotify is not installed.</p> <p>OwnTone was either built without support for Spotify or libspotify is not installed.</p>
</div> </div>
@ -56,7 +56,7 @@
</p> </p>
<p class="help is-danger" v-if="spotify_missing_scope.length > 0"> <p class="help is-danger" v-if="spotify_missing_scope.length > 0">
Please reauthorize Web API access to grant OwnTone the following additional access rights: Please reauthorize Web API access to grant OwnTone the following additional access rights:
<b><code>{{ spotify_missing_scope | join }}</code></b> <b><code>{{ spotify_missing_scope.join() }}</code></b>
</p> </p>
<div class="field fd-has-margin-top "> <div class="field fd-has-margin-top ">
<div class="control"> <div class="control">
@ -65,7 +65,7 @@
</div> </div>
<p class="help"> <p class="help">
Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are
<code>{{ spotify_required_scope | join }}</code>. <code>{{ spotify_required_scope.join() }}</code>.
</p> </p>
<div v-if="spotify.webapi_token_valid" class="field fd-has-margin-top "> <div v-if="spotify.webapi_token_valid" class="field fd-has-margin-top ">
<div class="control"> <div class="control">
@ -78,11 +78,11 @@
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Last.fm</div> <div class="title is-4">Last.fm</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<div class="notification is-size-7" v-if="!lastfm.enabled"> <div class="notification is-size-7" v-if="!lastfm.enabled">
<p>OwnTone was built without support for Last.fm.</p> <p>OwnTone was built without support for Last.fm.</p>
</div> </div>
@ -121,8 +121,8 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings' import TabsSettings from '@/components/TabsSettings.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-settings></tabs-settings> <tabs-settings></tabs-settings>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Remote Pairing</div> <div class="title is-4">Remote Pairing</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<!-- Paring request active --> <!-- Paring request active -->
<div class="notification" v-if="pairing.active"> <div class="notification" v-if="pairing.active">
<form v-on:submit.prevent="kickoff_pairing"> <form v-on:submit.prevent="kickoff_pairing">
@ -32,11 +32,11 @@
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Speaker pairing and device verification</div> <div class="title is-4">Speaker pairing and device verification</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="content"> <p class="content">
If your speaker requires pairing then activate it below and enter the PIN that it displays. If your speaker requires pairing then activate it below and enter the PIN that it displays.
</p> </p>
@ -66,8 +66,8 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings' import TabsSettings from '@/components/TabsSettings.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-settings></tabs-settings> <tabs-settings></tabs-settings>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Navbar items</div> <div class="title is-4">Navbar items</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="content"> <p class="content">
Select the top navigation bar menu items Select the top navigation bar menu items
</p> </p>
@ -15,56 +15,56 @@
If you select more items than can be shown on your screen then the burger menu will disappear. If you select more items than can be shown on your screen then the burger menu will disappear.
</div> </div>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_playlists"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_playlists">
<template slot="label"> Playlists</template> <template v-slot:label> Playlists</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_music"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_music">
<template slot="label"> Music</template> <template v-slot:label> Music</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_podcasts"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_podcasts">
<template slot="label"> Podcasts</template> <template v-slot:label> Podcasts</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_audiobooks"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_audiobooks">
<template slot="label"> Audiobooks</template> <template v-slot:label> Audiobooks</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_radio"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_radio">
<template slot="label"> Radio</template> <template v-slot:label> Radio</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_files"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_files">
<template slot="label"> Files</template> <template v-slot:label> Files</template>
</settings-checkbox> </settings-checkbox>
<settings-checkbox category_name="webinterface" option_name="show_menu_item_search"> <settings-checkbox category_name="webinterface" option_name="show_menu_item_search">
<template slot="label"> Search</template> <template v-slot:label> Search</template>
</settings-checkbox> </settings-checkbox>
</template> </template>
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Album lists</div> <div class="title is-4">Album lists</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<settings-checkbox category_name="webinterface" option_name="show_cover_artwork_in_album_lists"> <settings-checkbox category_name="webinterface" option_name="show_cover_artwork_in_album_lists">
<template slot="label"> Show cover artwork in album list</template> <template v-slot:label> Show cover artwork in album list</template>
</settings-checkbox> </settings-checkbox>
</template> </template>
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Now playing page</div> <div class="title is-4">Now playing page</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<settings-checkbox category_name="webinterface" option_name="show_composer_now_playing"> <settings-checkbox category_name="webinterface" option_name="show_composer_now_playing">
<template slot="label"> Show composer</template> <template v-slot:label> Show composer</template>
<template slot="info">If enabled the composer of the current playing track is shown on the &quot;now playing page&quot;</template> <template v-slot:info>If enabled the composer of the current playing track is shown on the &quot;now playing page&quot;</template>
</settings-checkbox> </settings-checkbox>
<settings-textfield category_name="webinterface" option_name="show_composer_for_genre" <settings-textfield category_name="webinterface" option_name="show_composer_for_genre"
:disabled="!settings_option_show_composer_now_playing" :disabled="!settings_option_show_composer_now_playing"
placeholder="Genres"> placeholder="Genres">
<template slot="label">Show composer only for listed genres</template> <template v-slot:label>Show composer only for listed genres</template>
<template slot="info"> <template v-slot:info>
<p class="help"> <p class="help">
Comma separated list of genres the composer should be displayed on the &quot;now playing page&quot;. Comma separated list of genres the composer should be displayed on the &quot;now playing page&quot;.
</p> </p>
@ -82,13 +82,13 @@
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">Recently added page</div> <div class="title is-4">Recently added page</div>
</template> </template>
<template slot="content"> <template v-slot:content>
<settings-intfield category_name="webinterface" option_name="recently_added_limit"> <settings-intfield category_name="webinterface" option_name="recently_added_limit">
<template slot="label">Limit the number of albums shown on the "Recently Added" page</template> <template v-slot:label>Limit the number of albums shown on the "Recently Added" page</template>
</settings-intfield> </settings-intfield>
</template> </template>
</content-with-heading> </content-with-heading>
@ -96,11 +96,11 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings' import TabsSettings from '@/components/TabsSettings.vue'
import SettingsCheckbox from '@/components/SettingsCheckbox' import SettingsCheckbox from '@/components/SettingsCheckbox.vue'
import SettingsTextfield from '@/components/SettingsTextfield' import SettingsTextfield from '@/components/SettingsTextfield.vue'
import SettingsIntfield from '@/components/SettingsIntfield' import SettingsIntfield from '@/components/SettingsIntfield.vue'
export default { export default {
name: 'SettingsPageWebinterface', name: 'SettingsPageWebinterface',

View File

@ -1,6 +1,6 @@
<template> <template>
<content-with-hero> <content-with-hero>
<template slot="heading-left"> <template v-slot:heading-left>
<h1 class="title is-5">{{ album.name }}</h1> <h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artists[0].name }}</a></h2> <h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artists[0].name }}</a></h2>
@ -14,7 +14,7 @@
</div> </div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<p class="image is-square fd-has-shadow fd-has-action"> <p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="artwork_url" :artwork_url="artwork_url"
@ -24,10 +24,10 @@
</p> </p>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.tracks.total }} tracks</p> <p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.tracks.total }} tracks</p>
<spotify-list-item-track v-for="(track, index) in album.tracks.items" :key="track.id" :track="track" :position="index" :album="album" :context_uri="album.uri"> <spotify-list-item-track v-for="(track, index) in album.tracks.items" :key="track.id" :track="track" :position="index" :album="album" :context_uri="album.uri">
<template slot="actions"> <template v-slot:actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -40,17 +40,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHero from '@/templates/ContentWithHero.vue'
import ContentWithHero from '@/templates/ContentWithHero' import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack' import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack' import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import store from '@/store' import store from '@/store'
import webapi from '@/webapi' import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
const albumData = { const dataObject = {
load: function (to) { load: function (to) {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token) spotifyApi.setAccessToken(store.state.spotify.webapi_token)
@ -64,7 +63,6 @@ const albumData = {
export default { export default {
name: 'PageAlbum', name: 'PageAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHero, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum, CoverArtwork }, components: { ContentWithHero, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum, CoverArtwork },
data () { data () {
@ -101,6 +99,19 @@ export default {
this.selected_track = track this.selected_track = track
this.show_track_details_modal = true this.show_track_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p> <p class="title is-4">{{ artist.name }}</p>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,13 +13,13 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ total }} albums</p> <p class="heading has-text-centered-mobile">{{ total }} albums</p>
<spotify-list-item-album v-for="album in albums" <spotify-list-item-album v-for="album in albums"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <template v-slot:artwork v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="artwork_url(album)" :artwork_url="artwork_url(album)"
@ -29,13 +29,13 @@
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_dialog(album)"> <a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-album> </spotify-list-item-album>
<infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="offset < total" :load="load_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" /> <spotify-modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
<spotify-modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" /> <spotify-modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
</template> </template>
@ -43,24 +43,25 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum' import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist.vue'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import store from '@/store' import store from '@/store'
import webapi from '@/webapi' import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import InfiniteLoading from 'vue-infinite-loading' import { VueEternalLoading } from '@ts-pro/vue-eternal-loading'
const artistData = { const PAGE_SIZE = 50
const dataObject = {
load: function (to) { load: function (to) {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token) spotifyApi.setAccessToken(store.state.spotify.webapi_token)
return Promise.all([ return Promise.all([
spotifyApi.getArtist(to.params.artist_id), spotifyApi.getArtist(to.params.artist_id),
spotifyApi.getArtistAlbums(to.params.artist_id, { limit: 50, offset: 0, include_groups: 'album,single', market: store.state.spotify.webapi_country }) spotifyApi.getArtistAlbums(to.params.artist_id, { limit: PAGE_SIZE, offset: 0, include_groups: 'album,single', market: store.state.spotify.webapi_country })
]) ])
}, },
@ -76,8 +77,7 @@ const artistData = {
export default { export default {
name: 'SpotifyPageArtist', name: 'SpotifyPageArtist',
mixins: [LoadDataBeforeEnterMixin(artistData)], components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, VueEternalLoading, CoverArtwork },
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading, CoverArtwork },
data () { data () {
return { return {
@ -100,25 +100,19 @@ export default {
}, },
methods: { methods: {
load_next: function ($state) { load_next: function ({ loaded }) {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token) spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getArtistAlbums(this.artist.id, { limit: 50, offset: this.offset, include_groups: 'album,single' }).then(data => { spotifyApi.getArtistAlbums(this.artist.id, { limit: PAGE_SIZE, offset: this.offset, include_groups: 'album,single' }).then(data => {
this.append_albums(data, $state) this.append_albums(data)
loaded(data.items.length, PAGE_SIZE)
}) })
}, },
append_albums: function (data, $state) { append_albums: function (data) {
this.albums = this.albums.concat(data.items) this.albums = this.albums.concat(data.items)
this.total = data.total this.total = data.total
this.offset += data.limit this.offset += data.limit
if ($state) {
$state.loaded()
if (this.offset >= this.total) {
$state.complete()
}
}
}, },
play: function () { play: function () {
@ -141,6 +135,19 @@ export default {
} }
return '' return ''
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,18 +1,18 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<!-- New Releases --> <!-- New Releases -->
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">New Releases</p> <p class="title is-4">New Releases</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-album v-for="album in new_releases" <spotify-list-item-album v-for="album in new_releases"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <template v-slot:artwork v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="artwork_url(album)" :artwork_url="artwork_url(album)"
@ -22,7 +22,7 @@
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_album_dialog(album)"> <a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -30,7 +30,7 @@
</spotify-list-item-album> </spotify-list-item-album>
<spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" /> <spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<router-link to="/music/spotify/new-releases" class="button is-light is-small is-rounded"> <router-link to="/music/spotify/new-releases" class="button is-light is-small is-rounded">
@ -43,12 +43,12 @@
<!-- Featured Playlists --> <!-- Featured Playlists -->
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Featured Playlists</p> <p class="title is-4">Featured Playlists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist"> <spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist">
<template slot="actions"> <template v-slot:actions>
<a @click="open_playlist_dialog(playlist)"> <a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -56,7 +56,7 @@
</spotify-list-item-playlist> </spotify-list-item-playlist>
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" /> <spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<router-link to="/music/spotify/featured-playlists" class="button is-light is-small is-rounded"> <router-link to="/music/spotify/featured-playlists" class="button is-light is-small is-rounded">
@ -70,19 +70,18 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist.vue'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum' import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
const browseData = { const dataObject = {
load: function (to) { load: function (to) {
if (store.state.spotify_new_releases.length > 0 && store.state.spotify_featured_playlists.length > 0) { if (store.state.spotify_new_releases.length > 0 && store.state.spotify_featured_playlists.length > 0) {
return Promise.resolve() return Promise.resolve()
@ -106,7 +105,6 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowse', name: 'SpotifyPageBrowse',
mixins: [LoadDataBeforeEnterMixin(browseData)],
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, CoverArtwork }, components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, CoverArtwork },
data () { data () {
@ -155,6 +153,19 @@ export default {
} }
return '' return ''
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Featured Playlists</p> <p class="title is-4">Featured Playlists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist"> <spotify-list-item-playlist v-for="playlist in featured_playlists" :key="playlist.id" :playlist="playlist">
<template slot="actions"> <template v-slot:actions>
<a @click="open_playlist_dialog(playlist)"> <a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -21,16 +21,15 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist.vue'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
const browseData = { const dataObject = {
load: function (to) { load: function (to) {
if (store.state.spotify_featured_playlists.length > 0) { if (store.state.spotify_featured_playlists.length > 0) {
return Promise.resolve() return Promise.resolve()
@ -50,7 +49,6 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowseFeaturedPlaylists', name: 'SpotifyPageBrowseFeaturedPlaylists',
mixins: [LoadDataBeforeEnterMixin(browseData)],
components: { ContentWithHeading, TabsMusic, SpotifyListItemPlaylist, SpotifyModalDialogPlaylist }, components: { ContentWithHeading, TabsMusic, SpotifyListItemPlaylist, SpotifyModalDialogPlaylist },
data () { data () {
@ -71,6 +69,19 @@ export default {
this.selected_playlist = playlist this.selected_playlist = playlist
this.show_playlist_details_modal = true this.show_playlist_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music></tabs-music> <tabs-music></tabs-music>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">New Releases</p> <p class="title is-4">New Releases</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-album v-for="album in new_releases" <spotify-list-item-album v-for="album in new_releases"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <template v-slot:artwork v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="artwork_url(album)" :artwork_url="artwork_url(album)"
@ -21,7 +21,7 @@
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_album_dialog(album)"> <a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
@ -34,17 +34,16 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import TabsMusic from '@/components/TabsMusic.vue'
import TabsMusic from '@/components/TabsMusic' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum' import CoverArtwork from '@/components/CoverArtwork.vue'
import CoverArtwork from '@/components/CoverArtwork'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
const browseData = { const dataObject = {
load: function (to) { load: function (to) {
if (store.state.spotify_new_releases.length > 0) { if (store.state.spotify_new_releases.length > 0) {
return Promise.resolve() return Promise.resolve()
@ -64,7 +63,6 @@ const browseData = {
export default { export default {
name: 'SpotifyPageBrowseNewReleases', name: 'SpotifyPageBrowseNewReleases',
mixins: [LoadDataBeforeEnterMixin(browseData)],
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum, CoverArtwork }, components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum, CoverArtwork },
data () { data () {
@ -101,6 +99,19 @@ export default {
} }
return '' return ''
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<content-with-heading> <content-with-heading>
<template slot="heading-left"> <template v-slot:heading-left>
<div class="title is-4">{{ playlist.name }}</div> <div class="title is-4">{{ playlist.name }}</div>
</template> </template>
<template slot="heading-right"> <template v-slot:heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true"> <a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span> <span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,16 +13,16 @@
</a> </a>
</div> </div>
</template> </template>
<template slot="content"> <template v-slot:content>
<p class="heading has-text-centered-mobile">{{ playlist.tracks.total }} tracks</p> <p class="heading has-text-centered-mobile">{{ playlist.tracks.total }} tracks</p>
<spotify-list-item-track v-for="(item, index) in tracks" :key="item.track.id" :track="item.track" :album="item.track.album" :position="index" :context_uri="playlist.uri"> <spotify-list-item-track v-for="(item, index) in tracks" :key="item.track.id" :track="item.track" :album="item.track.album" :position="index" :context_uri="playlist.uri">
<template slot="actions"> <template v-slot:actions>
<a @click="open_track_dialog(item.track)"> <a @click="open_track_dialog(item.track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-track> </spotify-list-item-track>
<infinite-loading v-if="offset < total" @infinite="load_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="offset < total" :load="load_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" /> <spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" />
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" /> <spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" />
</template> </template>
@ -30,23 +30,24 @@
</template> </template>
<script> <script>
import { LoadDataBeforeEnterMixin } from './mixin' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentWithHeading from '@/templates/ContentWithHeading' import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack' import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack' import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import store from '@/store' import store from '@/store'
import webapi from '@/webapi' import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import InfiniteLoading from 'vue-infinite-loading' import { VueEternalLoading } from '@ts-pro/vue-eternal-loading'
const playlistData = { const PAGE_SIZE = 50
const dataObject = {
load: function (to) { load: function (to) {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token) spotifyApi.setAccessToken(store.state.spotify.webapi_token)
return Promise.all([ return Promise.all([
spotifyApi.getPlaylist(to.params.playlist_id), spotifyApi.getPlaylist(to.params.playlist_id),
spotifyApi.getPlaylistTracks(to.params.playlist_id, { limit: 50, offset: 0 }) spotifyApi.getPlaylistTracks(to.params.playlist_id, { limit: PAGE_SIZE, offset: 0 })
]) ])
}, },
@ -61,8 +62,7 @@ const playlistData = {
export default { export default {
name: 'SpotifyPagePlaylist', name: 'SpotifyPagePlaylist',
mixins: [LoadDataBeforeEnterMixin(playlistData)], components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogPlaylist, VueEternalLoading },
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogPlaylist, InfiniteLoading },
data () { data () {
return { return {
@ -79,25 +79,19 @@ export default {
}, },
methods: { methods: {
load_next: function ($state) { load_next: function ({ loaded }) {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token) spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getPlaylistTracks(this.playlist.id, { limit: 50, offset: this.offset }).then(data => { spotifyApi.getPlaylistTracks(this.playlist.id, { limit: PAGE_SIZE, offset: this.offset }).then(data => {
this.append_tracks(data, $state) this.append_tracks(data)
loaded(data.items.length, PAGE_SIZE)
}) })
}, },
append_tracks: function (data, $state) { append_tracks: function (data) {
this.tracks = this.tracks.concat(data.items) this.tracks = this.tracks.concat(data.items)
this.total = data.total this.total = data.total
this.offset += data.limit this.offset += data.limit
if ($state) {
$state.loaded()
if (this.offset >= this.total) {
$state.complete()
}
}
}, },
play: function () { play: function () {
@ -109,6 +103,19 @@ export default {
this.selected_track = track this.selected_track = track
this.show_track_details_modal = true this.show_track_details_modal = true
} }
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
} }
} }
</script> </script>

View File

@ -27,21 +27,21 @@
<!-- Tracks --> <!-- Tracks -->
<content-with-heading v-if="show_tracks && tracks.total"> <content-with-heading v-if="show_tracks && tracks.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Tracks</p> <p class="title is-4">Tracks</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-track v-for="track in tracks.items" :key="track.id" :track="track" :album="track.album" :position="0" :context_uri="track.uri"> <spotify-list-item-track v-for="track in tracks.items" :key="track.id" :track="track" :album="track.album" :position="0" :context_uri="track.uri">
<template slot="actions"> <template v-slot:actions>
<a @click="open_track_dialog(track)"> <a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-track> </spotify-list-item-track>
<infinite-loading v-if="query.type === 'track'" @infinite="search_tracks_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="query.type === 'track'" :load="search_tracks_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" /> <spotify-modal-dialog-track :show="show_track_details_modal" :track="selected_track" :album="selected_track.album" @close="show_track_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_tracks_button" class="level"> <nav v-if="show_all_tracks_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a>
@ -50,28 +50,28 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_tracks && !tracks.total" class="mt-6"> <content-text v-if="show_tracks && !tracks.total" class="mt-6">
<template slot="content"> <template v-slot:content>
<p><i>No tracks found</i></p> <p><i>No tracks found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Artists --> <!-- Artists -->
<content-with-heading v-if="show_artists && artists.total"> <content-with-heading v-if="show_artists && artists.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Artists</p> <p class="title is-4">Artists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist"> <spotify-list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist">
<template slot="actions"> <template v-slot:actions>
<a @click="open_artist_dialog(artist)"> <a @click="open_artist_dialog(artist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-artist> </spotify-list-item-artist>
<infinite-loading v-if="query.type === 'artist'" @infinite="search_artists_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="query.type === 'artist'" :load="search_artists_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-artist :show="show_artist_details_modal" :artist="selected_artist" @close="show_artist_details_modal = false" /> <spotify-modal-dialog-artist :show="show_artist_details_modal" :artist="selected_artist" @close="show_artist_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_artists_button" class="level"> <nav v-if="show_all_artists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a>
@ -80,22 +80,22 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_artists && !artists.total"> <content-text v-if="show_artists && !artists.total">
<template slot="content"> <template v-slot:content>
<p><i>No artists found</i></p> <p><i>No artists found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Albums --> <!-- Albums -->
<content-with-heading v-if="show_albums && albums.total"> <content-with-heading v-if="show_albums && albums.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Albums</p> <p class="title is-4">Albums</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-album v-for="album in albums.items" <spotify-list-item-album v-for="album in albums.items"
:key="album.id" :key="album.id"
:album="album" :album="album"
@click="open_album(album)"> @click="open_album(album)">
<template slot="artwork" v-if="is_visible_artwork"> <template v-slot:artwork v-if="is_visible_artwork">
<p class="image is-64x64 fd-has-shadow fd-has-action"> <p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork <cover-artwork
:artwork_url="artwork_url(album)" :artwork_url="artwork_url(album)"
@ -105,16 +105,16 @@
:maxheight="64" /> :maxheight="64" />
</p> </p>
</template> </template>
<template slot="actions"> <template v-slot:actions>
<a @click="open_album_dialog(album)"> <a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-album> </spotify-list-item-album>
<infinite-loading v-if="query.type === 'album'" @infinite="search_albums_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="query.type === 'album'" :load="search_albums_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" /> <spotify-modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_albums_button" class="level"> <nav v-if="show_all_albums_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a>
@ -123,28 +123,28 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_albums && !albums.total"> <content-text v-if="show_albums && !albums.total">
<template slot="content"> <template v-slot:content>
<p><i>No albums found</i></p> <p><i>No albums found</i></p>
</template> </template>
</content-text> </content-text>
<!-- Playlists --> <!-- Playlists -->
<content-with-heading v-if="show_playlists && playlists.total"> <content-with-heading v-if="show_playlists && playlists.total">
<template slot="heading-left"> <template v-slot:heading-left>
<p class="title is-4">Playlists</p> <p class="title is-4">Playlists</p>
</template> </template>
<template slot="content"> <template v-slot:content>
<spotify-list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist"> <spotify-list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist">
<template slot="actions"> <template v-slot:actions>
<a @click="open_playlist_dialog(playlist)"> <a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span> <span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a> </a>
</template> </template>
</spotify-list-item-playlist> </spotify-list-item-playlist>
<infinite-loading v-if="query.type === 'playlist'" @infinite="search_playlists_next"><span slot="no-more">.</span></infinite-loading> <VueEternalLoading v-if="query.type === 'playlist'" :load="search_playlists_next"><template #no-more>.</template></VueEternalLoading>
<spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" /> <spotify-modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
</template> </template>
<template slot="footer"> <template v-slot:footer>
<nav v-if="show_all_playlists_button" class="level"> <nav v-if="show_all_playlists_button" class="level">
<p class="level-item"> <p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a> <a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a>
@ -153,7 +153,7 @@
</template> </template>
</content-with-heading> </content-with-heading>
<content-text v-if="show_playlists && !playlists.total"> <content-text v-if="show_playlists && !playlists.total">
<template slot="content"> <template v-slot:content>
<p><i>No playlists found</i></p> <p><i>No playlists found</i></p>
</template> </template>
</content-text> </content-text>
@ -161,26 +161,28 @@
</template> </template>
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentText from '@/templates/ContentText' import ContentText from '@/templates/ContentText.vue'
import TabsSearch from '@/components/TabsSearch' import TabsSearch from '@/components/TabsSearch.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack' import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyListItemArtist from '@/components/SpotifyListItemArtist' import SpotifyListItemArtist from '@/components/SpotifyListItemArtist.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum' import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist' import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack' import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist' import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum' import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist' import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import CoverArtwork from '@/components/CoverArtwork' import CoverArtwork from '@/components/CoverArtwork.vue'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import webapi from '@/webapi' import webapi from '@/webapi'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import InfiniteLoading from 'vue-infinite-loading' import { VueEternalLoading } from '@ts-pro/vue-eternal-loading'
const PAGE_SIZE = 50
export default { export default {
name: 'SpotifyPageSearch', name: 'SpotifyPageSearch',
components: { ContentWithHeading, ContentText, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogTrack, SpotifyModalDialogArtist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, InfiniteLoading, CoverArtwork }, components: { ContentWithHeading, ContentText, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogTrack, SpotifyModalDialogArtist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, VueEternalLoading, CoverArtwork },
data () { data () {
return { return {
@ -266,7 +268,7 @@ export default {
} }
this.search_query = this.query.query this.search_query = this.query.query
this.search_param.limit = this.query.limit ? this.query.limit : 50 this.search_param.limit = this.query.limit ? this.query.limit : PAGE_SIZE
this.search_param.offset = this.query.offset ? this.query.offset : 0 this.search_param.offset = this.query.offset ? this.query.offset : 0
this.$store.commit(types.ADD_RECENT_SEARCH, this.query.query) this.$store.commit(types.ADD_RECENT_SEARCH, this.query.query)
@ -295,55 +297,43 @@ export default {
}) })
}, },
search_tracks_next: function ($state) { search_tracks_next: function ({ loaded }) {
this.spotify_search().then(data => { this.spotify_search().then(data => {
this.tracks.items = this.tracks.items.concat(data.tracks.items) this.tracks.items = this.tracks.items.concat(data.tracks.items)
this.tracks.total = data.tracks.total this.tracks.total = data.tracks.total
this.search_param.offset += data.tracks.limit this.search_param.offset += data.tracks.limit
$state.loaded() loaded(data.tracks.items.length, PAGE_SIZE)
if (this.search_param.offset >= this.tracks.total) {
$state.complete()
}
}) })
}, },
search_artists_next: function ($state) { search_artists_next: function ({ loaded }) {
this.spotify_search().then(data => { this.spotify_search().then(data => {
this.artists.items = this.artists.items.concat(data.artists.items) this.artists.items = this.artists.items.concat(data.artists.items)
this.artists.total = data.artists.total this.artists.total = data.artists.total
this.search_param.offset += data.artists.limit this.search_param.offset += data.artists.limit
$state.loaded() loaded(data.artists.items.length, PAGE_SIZE)
if (this.search_param.offset >= this.artists.total) {
$state.complete()
}
}) })
}, },
search_albums_next: function ($state) { search_albums_next: function ({ loaded }) {
this.spotify_search().then(data => { this.spotify_search().then(data => {
this.albums.items = this.albums.items.concat(data.albums.items) this.albums.items = this.albums.items.concat(data.albums.items)
this.albums.total = data.albums.total this.albums.total = data.albums.total
this.search_param.offset += data.albums.limit this.search_param.offset += data.albums.limit
$state.loaded() loaded(data.albums.items.length, PAGE_SIZE)
if (this.search_param.offset >= this.albums.total) {
$state.complete()
}
}) })
}, },
search_playlists_next: function ($state) { search_playlists_next: function ({ loaded }) {
this.spotify_search().then(data => { this.spotify_search().then(data => {
this.playlists.items = this.playlists.items.concat(data.playlists.items) this.playlists.items = this.playlists.items.concat(data.playlists.items)
this.playlists.total = data.playlists.total this.playlists.total = data.playlists.total
this.search_param.offset += data.playlists.limit this.search_param.offset += data.playlists.limit
$state.loaded() loaded(data.playlists.items.length, PAGE_SIZE)
if (this.search_param.offset >= this.playlists.total) {
$state.complete()
}
}) })
}, },

View File

@ -1,17 +0,0 @@
export const LoadDataBeforeEnterMixin = function (dataObject) {
return {
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
}
}
}

View File

@ -1,8 +0,0 @@
import Vue from 'vue'
import VueProgressBar from 'vue-progressbar'
Vue.use(VueProgressBar, {
color: 'hsl(204, 86%, 53%)',
failedColor: 'red',
height: '1px'
})

View File

@ -1,50 +1,48 @@
import Vue from 'vue' import { createRouter, createWebHashHistory } from 'vue-router'
import VueRouter from 'vue-router'
import store from '@/store' import store from '@/store'
import * as types from '@/store/mutation_types' import * as types from '@/store/mutation_types'
import PageQueue from '@/pages/PageQueue' import PageQueue from '@/pages/PageQueue.vue'
import PageNowPlaying from '@/pages/PageNowPlaying' import PageNowPlaying from '@/pages/PageNowPlaying.vue'
import PageBrowse from '@/pages/PageBrowse' import PageBrowse from '@/pages/PageBrowse.vue'
import PageBrowseRecentlyAdded from '@/pages/PageBrowseRecentlyAdded' import PageBrowseRecentlyAdded from '@/pages/PageBrowseRecentlyAdded.vue'
import PageBrowseRecentlyPlayed from '@/pages/PageBrowseRecentlyPlayed' import PageBrowseRecentlyPlayed from '@/pages/PageBrowseRecentlyPlayed.vue'
import PageArtists from '@/pages/PageArtists' import PageArtists from '@/pages/PageArtists.vue'
import PageArtist from '@/pages/PageArtist' import PageArtist from '@/pages/PageArtist.vue'
import PageAlbums from '@/pages/PageAlbums' import PageAlbums from '@/pages/PageAlbums.vue'
import PageAlbum from '@/pages/PageAlbum' import PageAlbum from '@/pages/PageAlbum.vue'
import PageGenres from '@/pages/PageGenres' import PageGenres from '@/pages/PageGenres.vue'
import PageGenre from '@/pages/PageGenre' import PageGenre from '@/pages/PageGenre.vue'
import PageGenreTracks from '@/pages/PageGenreTracks' import PageGenreTracks from '@/pages/PageGenreTracks.vue'
import PageArtistTracks from '@/pages/PageArtistTracks' import PageArtistTracks from '@/pages/PageArtistTracks.vue'
import PageComposers from '@/pages/PageComposers' import PageComposers from '@/pages/PageComposers.vue'
import PageComposer from '@/pages/PageComposer' import PageComposer from '@/pages/PageComposer.vue'
import PageComposerTracks from '@/pages/PageComposerTracks' import PageComposerTracks from '@/pages/PageComposerTracks.vue'
import PagePodcasts from '@/pages/PagePodcasts' import PagePodcasts from '@/pages/PagePodcasts.vue'
import PagePodcast from '@/pages/PagePodcast' import PagePodcast from '@/pages/PagePodcast.vue'
import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums' import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums.vue'
import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists' import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists.vue'
import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist' import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist.vue'
import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum' import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum.vue'
import PagePlaylists from '@/pages/PagePlaylists' import PagePlaylists from '@/pages/PagePlaylists.vue'
import PagePlaylist from '@/pages/PagePlaylist' import PagePlaylist from '@/pages/PagePlaylist.vue'
import PageFiles from '@/pages/PageFiles' import PageFiles from '@/pages/PageFiles.vue'
import PageRadioStreams from '@/pages/PageRadioStreams' import PageRadioStreams from '@/pages/PageRadioStreams.vue'
import PageSearch from '@/pages/PageSearch' import PageSearch from '@/pages/PageSearch.vue'
import PageAbout from '@/pages/PageAbout' import PageAbout from '@/pages/PageAbout.vue'
import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse' import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse.vue'
import SpotifyPageBrowseNewReleases from '@/pages/SpotifyPageBrowseNewReleases' import SpotifyPageBrowseNewReleases from '@/pages/SpotifyPageBrowseNewReleases.vue'
import SpotifyPageBrowseFeaturedPlaylists from '@/pages/SpotifyPageBrowseFeaturedPlaylists' import SpotifyPageBrowseFeaturedPlaylists from '@/pages/SpotifyPageBrowseFeaturedPlaylists.vue'
import SpotifyPageArtist from '@/pages/SpotifyPageArtist' import SpotifyPageArtist from '@/pages/SpotifyPageArtist.vue'
import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum' import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum.vue'
import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist' import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist.vue'
import SpotifyPageSearch from '@/pages/SpotifyPageSearch' import SpotifyPageSearch from '@/pages/SpotifyPageSearch.vue'
import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface' import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface.vue'
import SettingsPageArtwork from '@/pages/SettingsPageArtwork' import SettingsPageArtwork from '@/pages/SettingsPageArtwork.vue'
import SettingsPageOnlineServices from '@/pages/SettingsPageOnlineServices' import SettingsPageOnlineServices from '@/pages/SettingsPageOnlineServices.vue'
import SettingsPageRemotesOutputs from '@/pages/SettingsPageRemotesOutputs' import SettingsPageRemotesOutputs from '@/pages/SettingsPageRemotesOutputs.vue'
Vue.use(VueRouter) export const router = createRouter({
history: createWebHashHistory(),
export const router = new VueRouter({
routes: [ routes: [
{ {
path: '/', path: '/',
@ -289,34 +287,47 @@ export const router = new VueRouter({
} }
], ],
scrollBehavior (to, from, savedPosition) { scrollBehavior (to, from, savedPosition) {
const wait_ms = 0
// console.log(to.path + '_' + from.path + '__' + to.hash + ' savedPosition:' + savedPosition) // console.log(to.path + '_' + from.path + '__' + to.hash + ' savedPosition:' + savedPosition)
if (savedPosition) { if (savedPosition) {
// We have saved scroll position (browser back/forward navigation), use this position
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
resolve(savedPosition) resolve(savedPosition)
}, 10) }, wait_ms)
}) })
} else if (to.path === from.path && to.hash) { }
return { selector: to.hash, offset: { x: 0, y: 120 } }
} else if (to.hash) { if (to.path === from.path && to.hash) {
// We are staying on the same page and are jumping to an anker (e. g. index nav)
// We don't have a transition, so don't add a timeout!
return { el: to.hash, top: 120 }
}
if (to.hash) {
// We are navigating to an anker of a new page, add a timeout to let the transition effect finish before scrolling
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
resolve({ selector: to.hash, offset: { x: 0, y: 120 } }) resolve({ el: to.hash, top: 120 })
}, 10) }, wait_ms)
}) })
} else if (to.meta.has_index) { }
if (to.meta.has_index) {
// We are navigating to a page with index nav, that should be hidden automatically
// Dependending on wether we have a tab navigation, add an offset to the "top" anker
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
if (to.meta.has_tabs) { if (to.meta.has_tabs) {
resolve({ selector: '#top', offset: { x: 0, y: 140 } }) resolve({ el: '#top', top: 140 })
} else { } else {
resolve({ selector: '#top', offset: { x: 0, y: 100 } }) resolve({ el: '#top', top: 100 })
} }
}, 10) }, wait_ms)
}) })
} else {
return { x: 0, y: 0 }
} }
return { left: 0, top: 0 }
} }
}) })

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