[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>

36383
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",
"version": "1.2.0",
"private": true,
"description": "OwnTone web interface",
"author": "chme <christian.meffert@googlemail.com>",
"version": "2.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --no-clean --modern",
"lint": "vue-cli-service lint",
"dev": "vue-cli-service serve"
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "vite"
},
"dependencies": {
"@aacassandra/vue3-progressbar": "^1.0.3",
"@ts-pro/vue-eternal-loading": "^1.2.0",
"@vueform/slider": "^2.0.8",
"axios": "^0.25.0",
"bulma": "^0.9.3",
"bulma-switch": "^2.0.4",
"core-js": "^3.15.2",
"mdi": "^2.2.43",
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"npm": "^7.19.1",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2",
"string-to-color": "^2.2.2",
"v-click-outside": "^3.1.2",
"vue": "^2.6.14",
"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": "^3.2.31",
"vue-router": "^4.0.12",
"vue-scrollto": "^2.20.0",
"vue-tiny-lazyload-img": "^0.1.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
"vue3-click-away": "^1.2.1",
"vue3-lazyload": "^0.2.5-beta",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^5.0.0-rc.1",
"@vue/cli-service": "^4.5.15",
"@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"
"@vitejs/plugin-vue": "^2.2.0",
"sass": "^1.49.7",
"vite": "^2.8.1"
}
}

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">
<navbar-top />
<vue-progress-bar class="fd-progress-bar" />
<transition name="fade">
<!-- Setting v-show to true on the router-view tag avoids jumpiness during transitions -->
<router-view v-show="true" />
</transition>
<router-view v-slot="{ Component }">
<component :is="Component" class="fd-page" />
</router-view>
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" />
<modal-dialog-update
:show="show_update_dialog"
@ -18,11 +18,11 @@
</template>
<script>
import NavbarTop from '@/components/NavbarTop'
import NavbarBottom from '@/components/NavbarBottom'
import Notifications from '@/components/Notifications'
import ModalDialogRemotePairing from '@/components/ModalDialogRemotePairing'
import ModalDialogUpdate from '@/components/ModalDialogUpdate'
import NavbarTop from '@/components/NavbarTop.vue'
import NavbarBottom from '@/components/NavbarBottom.vue'
import Notifications from '@/components/Notifications.vue'
import ModalDialogRemotePairing from '@/components/ModalDialogRemotePairing.vue'
import ModalDialogUpdate from '@/components/ModalDialogUpdate.vue'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
import ReconnectingWebSocket from 'reconnectingwebsocket'
@ -125,9 +125,9 @@ export default {
}
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
wsUrl = process.env.VUE_APP_WEBSOCKET_SERVER
wsUrl = import.meta.env.VUE_APP_WEBSOCKET_SERVER
}
const socket = new ReconnectingWebSocket(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
</p>
<p v-if="album.date_released">
<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 v-else-if="album.year > 0">
<span class="heading">Year</span>
@ -37,7 +37,7 @@
</p>
<p>
<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>
<span class="heading">Type</span>
@ -45,7 +45,7 @@
</p>
<p>
<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>
</div>
</div>
@ -69,7 +69,7 @@
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork'
import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi'
export default {

View File

@ -24,7 +24,7 @@
</p>
<p>
<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>
</div>
</div>

View File

@ -41,7 +41,7 @@
</p>
<p>
<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>
<span class="heading">Path</span>
@ -56,7 +56,7 @@
<span class="title is-6">
{{ item.type }}
<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>
</p>

View File

@ -31,7 +31,7 @@
</p>
<p v-if="track.date_released">
<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 v-else-if="track.year > 0">
<span class="heading">Year</span>
@ -47,7 +47,7 @@
</p>
<p>
<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>
<span class="heading">Path</span>
@ -62,13 +62,13 @@
<span class="title is-6">
{{ track.type }}
<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>
</p>
<p>
<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>
<span class="heading">Rating</span>

View File

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

View File

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

View File

@ -14,7 +14,15 @@
<div class="level-item fd-expanded">
<div class="fd-expanded">
<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"
min="0"
max="100"
@ -22,7 +30,7 @@
:disabled="!output.selected"
:value="volume"
@change="set_volume" >
</range-slider>
</range-slider-->
</div>
</div>
</div>
@ -31,12 +39,16 @@
</template>
<script>
import RangeSlider from 'vue-range-slider'
//import RangeSlider from 'vue-range-slider'
import Slider from '@vueform/slider'
import webapi from '@/webapi'
export default {
name: 'NavbarItemOutput',
components: { RangeSlider },
components: {
// RangeSlider
Slider
},
props: ['output'],

View File

@ -79,7 +79,7 @@
</template>
<script>
import NavbarItemLink from './NavbarItemLink'
import NavbarItemLink from './NavbarItemLink.vue'
import * as types from '@/store/mutation_types'
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
const newValue = this.$refs.settings_checkbox.checked
console.log(this.$refs.settings_checkbox)
if (newValue === this.value) {
this.statusUpdate = ''
return

View File

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

View File

@ -19,7 +19,7 @@
</p>
<p>
<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>
<span class="heading">Type</span>

View File

@ -23,7 +23,7 @@
</p>
<p>
<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>
<span class="heading">Track / Disc</span>
@ -31,7 +31,7 @@
</p>
<p>
<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>
<span class="heading">Path</span>

View File

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

View File

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

View File

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

View File

@ -1,39 +1,41 @@
import Vue from 'vue'
import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format'
momentDurationFormatSetup(moment)
Vue.filter('duration', function (value, format) {
if (format) {
return moment.duration(value).format(format)
}
return moment.duration(value).format('hh:*mm:ss')
})
Vue.filter('time', function (value, format) {
if (format) {
return moment(value).format(format)
}
return moment(value).format()
})
export const filters = {
duration: function (value, format) {
if (format) {
return moment.duration(value).format(format)
}
return moment.duration(value).format('hh:*mm:ss')
},
Vue.filter('timeFromNow', function (value, withoutSuffix) {
return moment(value).fromNow(withoutSuffix)
})
time: function (value, format) {
if (format) {
return moment(value).format(format)
}
return moment(value).format()
},
Vue.filter('number', function (value) {
return value.toLocaleString()
})
timeFromNow: function (value, withoutSuffix) {
return moment(value).fromNow(withoutSuffix)
},
Vue.filter('channels', function (value) {
if (value === 1) {
return 'mono'
number: function (value) {
return value.toLocaleString()
},
channels: function (value) {
if (value === 1) {
return 'mono'
}
if (value === 2) {
return 'stereo'
}
if (!value) {
return ''
}
return value + ' channels'
}
if (value === 2) {
return 'stereo'
}
if (!value) {
return ''
}
return value + ' channels'
})
}

View File

@ -3,29 +3,67 @@
* Copyright (c) 2017 Adam Bender
* https://github.com/bendera/placeholder/blob/master/LICENSE
*/
class SVGRenderer {
render (data) {
const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' +
'<defs>' +
'<style type="text/css">' +
' #holder text {' +
' fill: ' + data.textColor + ';' +
' font-family: ' + data.fontFamily + ';' +
' font-size: ' + data.fontSize + 'px;' +
' font-weight: ' + data.fontWeight + ';' +
' }' +
' </style>' +
'</defs>' +
'<g id="holder">' +
' <rect width="100%" height="100%" fill="' + data.backgroundColor + '"></rect>' +
' <g>' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' + data.caption + '</text>' +
' </g>' +
'</g>' +
'</svg>'
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
}
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
}
export default SVGRenderer
function calc_text_color (background_color) {
return is_background_light(background_color) ? '#000000' : '#ffffff'
}
function createSVG (data) {
const svg = '<svg width="' + data.width + '" height="' + data.height + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + data.width + ' ' + data.height + '" preserveAspectRatio="none">' +
'<defs>' +
'<style type="text/css">' +
' #holder text {' +
' fill: ' + data.textColor + ';' +
' font-family: ' + data.fontFamily + ';' +
' font-size: ' + data.fontSize + 'px;' +
' font-weight: ' + data.fontWeight + ';' +
' }' +
' </style>' +
'</defs>' +
'<g id="holder">' +
' <rect width="100%" height="100%" fill="' + data.backgroundColor + '"></rect>' +
' <g>' +
' <text text-anchor="middle" x="50%" y="50%" dy=".3em">' + data.caption + '</text>' +
' </g>' +
'</g>' +
'</svg>'
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 { 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
// (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 { createApp } from 'vue'
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 { router } from './router'
import VueProgressBar from '@aacassandra/vue3-progressbar'
import VueClickAway from "vue3-click-away"
import VueLazyLoad from 'vue3-lazyload'
import VueScrollTo from 'vue-scrollto'
import 'mdi/css/materialdesignicons.css'
import 'vue-range-slider/dist/vue-range-slider.css'
import { filters } from './filter'
import App from './App.vue'
import './mystyles.scss'
import 'mdi/css/materialdesignicons.css'
import '@vueform/slider/themes/default.css'
Vue.config.productionTip = false
const app = createApp(App)
.use(store)
.use(router)
.use(VueProgressBar, {
color: 'hsl(204, 86%, 53%)',
failedColor: 'red',
height: '1px'
})
.use(VueClickAway)
.use(VueLazyLoad, {
// Do not log errors, if image does not exist
log: false
})
.use(VueScrollTo)
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/>'
})
app.config.globalProperties.$filters = filters
app.mount('#app')

View File

@ -1,34 +1,47 @@
@charset "utf-8";
@import 'bulma';
@import '~bulma-switch';
@import 'bulma/bulma.sass';
@import 'bulma-switch';
/* Volume slider */
.slider {
min-width: 250px;
width: 100%;
}
.range-slider-fill {
background-color: hsl(0, 0%, 21%);
margin-top: 16px;
margin-bottom: 16px;
--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 {
margin: 0;
padding: 0;
/* Now playing progress bar */
.seek-slider {
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 {
visibility: hidden;
}
.track-progress .range-slider-fill {
background-color: hsl(217, 71%, 53%);
height: 2px;
}
.track-progress .range-slider-rail {
background-color: hsl(0, 0%, 100%);
.progress-bar {
background-color: $info;
border-radius: 9999px;
height: 4px;
}
.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);
}
.fd-page {
margin-top: 3.25rem;
}
.fd-page-with-tabs {
margin-top: 6.25rem !important;
}
/* Set minimum height to hide "option" section */
.fd-content-with-option {
min-height: calc(100vh - 3.25rem - 3.25rem - 5rem);
@ -192,28 +213,17 @@ section.hero + section.fd-content {
}
/* Transition effect */
.fade-enter-active, .fade-leave-active {
transition: opacity .4s;
.fade-leave-active {
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;
}
/* Now playing progress bar */
.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%);
.fade-enter-to, .fade-leave-from {
opacity: 1;
}
/* Add a little bit of spacing between title and subtitle */

View File

@ -34,27 +34,27 @@
<tbody>
<tr>
<th>Artists</th>
<td class="has-text-right">{{ library.artists | number }}</td>
<td class="has-text-right">{{ $filters.number(library.artists) }}</td>
</tr>
<tr>
<th>Albums</th>
<td class="has-text-right">{{ library.albums | number }}</td>
<td class="has-text-right">{{ $filters.number(library.albums) }}</td>
</tr>
<tr>
<th>Tracks</th>
<td class="has-text-right">{{ library.songs | number }}</td>
<td class="has-text-right">{{ $filters.number(library.songs) }}</td>
</tr>
<tr>
<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>
<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>
<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>
</tbody>
</table>
@ -68,7 +68,7 @@
<div class="columns is-centered">
<div class="column is-four-fifths">
<div class="content has-text-centered-mobile">
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p>
<p class="is-size-7">Compiled with support for {{ config.buildoptions.join(', ') }}.</p>
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/owntone/owntone-server/network/dependencies">more</a>.</p>
</div>
</div>
@ -107,12 +107,6 @@ export default {
showUpdateDialog () {
this.$store.commit(types.SHOW_UPDATE_DIALOG, true)
}
},
filters: {
join: function (array) {
return array.join(', ')
}
}
}
</script>

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<template>
<div>
<content-with-heading>
<template slot="options">
<template v-slot:options>
<index-button-list :index="index_list"></index-button-list>
</template>
<template slot="heading-left">
<template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p>
</template>
<template slot="heading-right">
<template v-slot:heading-right>
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -17,7 +17,7 @@
</a>
</div>
</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>
<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" />
@ -27,14 +27,13 @@
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import IndexButtonList from '@/components/IndexButtonList'
import ListTracks from '@/components/ListTracks'
import ModalDialogArtist from '@/components/ModalDialogArtist'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListTracks from '@/components/ListTracks.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import webapi from '@/webapi'
const tracksData = {
const dataObject = {
load: function (to) {
return Promise.all([
webapi.library_artist(to.params.artist_id),
@ -50,7 +49,6 @@ const tracksData = {
export default {
name: 'PageArtistTracks',
mixins: [LoadDataBeforeEnterMixin(tracksData)],
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist },
data () {
@ -82,6 +80,19 @@ export default {
play: function () {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
<template>
<div>
<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>
</template>
<template slot="heading-right">
<template v-slot:heading-right>
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -14,10 +17,10 @@
</a>
</div>
</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>
<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)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
@ -31,14 +34,13 @@
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemAlbums from '@/components/ListItemAlbum'
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
import ModalDialogComposer from '@/components/ModalDialogComposer'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemAlbums from '@/components/ListItemAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import webapi from '@/webapi'
const composerData = {
const dataObject = {
load: function (to) {
return webapi.library_composer(to.params.composer)
},
@ -51,7 +53,6 @@ const composerData = {
export default {
name: 'PageComposer',
mixins: [LoadDataBeforeEnterMixin(composerData)],
components: { ContentWithHeading, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer },
data () {
@ -90,6 +91,19 @@ export default {
this.selected_album = album
this.show_details_modal = true
}
},
beforeRouteEnter (to, from, next) {
dataObject.load(to).then((response) => {
next(vm => dataObject.set(vm, response))
})
},
beforeRouteUpdate (to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
next()
})
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<template>
<div>
<div class="fd-page-with-tabs">
<tabs-settings></tabs-settings>
<content-with-heading>
<template slot="heading-left">
<template v-slot:heading-left>
<div class="title is-4">Artwork</div>
</template>
<template slot="content">
<template v-slot:content>
<div class="content">
<p>
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>
</div>
<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 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 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>
</template>
</content-with-heading>
@ -30,9 +30,9 @@
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsSettings from '@/components/TabsSettings'
import SettingsCheckbox from '@/components/SettingsCheckbox'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings.vue'
import SettingsCheckbox from '@/components/SettingsCheckbox.vue'
export default {
name: 'SettingsPageArtwork',

View File

@ -1,13 +1,13 @@
<template>
<div>
<div class="fd-page-with-tabs">
<tabs-settings></tabs-settings>
<content-with-heading>
<template slot="heading-left">
<template v-slot:heading-left>
<div class="title is-4">Spotify</div>
</template>
<template slot="content">
<template v-slot:content>
<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>
</div>
@ -56,7 +56,7 @@
</p>
<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:
<b><code>{{ spotify_missing_scope | join }}</code></b>
<b><code>{{ spotify_missing_scope.join() }}</code></b>
</p>
<div class="field fd-has-margin-top ">
<div class="control">
@ -65,7 +65,7 @@
</div>
<p class="help">
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>
<div v-if="spotify.webapi_token_valid" class="field fd-has-margin-top ">
<div class="control">
@ -78,11 +78,11 @@
</content-with-heading>
<content-with-heading>
<template slot="heading-left">
<template v-slot:heading-left>
<div class="title is-4">Last.fm</div>
</template>
<template slot="content">
<template v-slot:content>
<div class="notification is-size-7" v-if="!lastfm.enabled">
<p>OwnTone was built without support for Last.fm.</p>
</div>
@ -121,8 +121,8 @@
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsSettings from '@/components/TabsSettings'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsSettings from '@/components/TabsSettings.vue'
import webapi from '@/webapi'
export default {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<content-with-hero>
<template slot="heading-left">
<template v-slot:heading-left>
<h1 class="title is-5">{{ album.name }}</h1>
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artists[0].name }}</a></h2>
@ -14,7 +14,7 @@
</div>
</template>
<template slot="heading-right">
<template v-slot:heading-right>
<p class="image is-square fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="artwork_url"
@ -24,10 +24,10 @@
</p>
</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>
<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)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
@ -40,17 +40,16 @@
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHero from '@/templates/ContentWithHero'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import CoverArtwork from '@/components/CoverArtwork'
import ContentWithHero from '@/templates/ContentWithHero.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import CoverArtwork from '@/components/CoverArtwork.vue'
import store from '@/store'
import webapi from '@/webapi'
import SpotifyWebApi from 'spotify-web-api-js'
const albumData = {
const dataObject = {
load: function (to) {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token)
@ -64,7 +63,6 @@ const albumData = {
export default {
name: 'PageAlbum',
mixins: [LoadDataBeforeEnterMixin(albumData)],
components: { ContentWithHero, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogAlbum, CoverArtwork },
data () {
@ -101,6 +99,19 @@ export default {
this.selected_track = track
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>

View File

@ -1,9 +1,9 @@
<template>
<content-with-heading>
<template slot="heading-left">
<template v-slot:heading-left>
<p class="title is-4">{{ artist.name }}</p>
</template>
<template slot="heading-right">
<template v-slot:heading-right>
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,13 +13,13 @@
</a>
</div>
</template>
<template slot="content">
<template v-slot:content>
<p class="heading has-text-centered-mobile">{{ total }} albums</p>
<spotify-list-item-album v-for="album in albums"
:key="album.id"
: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">
<cover-artwork
:artwork_url="artwork_url(album)"
@ -29,13 +29,13 @@
:maxheight="64" />
</p>
</template>
<template slot="actions">
<a @click="open_dialog(album)">
<template v-slot:actions>
<a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
</template>
@ -43,24 +43,25 @@
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
import CoverArtwork from '@/components/CoverArtwork'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist.vue'
import CoverArtwork from '@/components/CoverArtwork.vue'
import store from '@/store'
import webapi from '@/webapi'
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) {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token)
return Promise.all([
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 {
name: 'SpotifyPageArtist',
mixins: [LoadDataBeforeEnterMixin(artistData)],
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading, CoverArtwork },
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, VueEternalLoading, CoverArtwork },
data () {
return {
@ -100,25 +100,19 @@ export default {
},
methods: {
load_next: function ($state) {
load_next: function ({ loaded }) {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getArtistAlbums(this.artist.id, { limit: 50, offset: this.offset, include_groups: 'album,single' }).then(data => {
this.append_albums(data, $state)
spotifyApi.getArtistAlbums(this.artist.id, { limit: PAGE_SIZE, offset: this.offset, include_groups: 'album,single' }).then(data => {
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.total = data.total
this.offset += data.limit
if ($state) {
$state.loaded()
if (this.offset >= this.total) {
$state.complete()
}
}
},
play: function () {
@ -141,6 +135,19 @@ export default {
}
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>

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
<template>
<content-with-heading>
<template slot="heading-left">
<template v-slot:heading-left>
<div class="title is-4">{{ playlist.name }}</div>
</template>
<template slot="heading-right">
<template v-slot:heading-right>
<div class="buttons is-centered">
<a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true">
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
@ -13,16 +13,16 @@
</a>
</div>
</template>
<template slot="content">
<template v-slot:content>
<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">
<template slot="actions">
<template v-slot:actions>
<a @click="open_track_dialog(item.track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" />
</template>
@ -30,23 +30,24 @@
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import store from '@/store'
import webapi from '@/webapi'
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) {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(store.state.spotify.webapi_token)
return Promise.all([
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 {
name: 'SpotifyPagePlaylist',
mixins: [LoadDataBeforeEnterMixin(playlistData)],
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogPlaylist, InfiniteLoading },
components: { ContentWithHeading, SpotifyListItemTrack, SpotifyModalDialogTrack, SpotifyModalDialogPlaylist, VueEternalLoading },
data () {
return {
@ -79,25 +79,19 @@ export default {
},
methods: {
load_next: function ($state) {
load_next: function ({ loaded }) {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
spotifyApi.getPlaylistTracks(this.playlist.id, { limit: 50, offset: this.offset }).then(data => {
this.append_tracks(data, $state)
spotifyApi.getPlaylistTracks(this.playlist.id, { limit: PAGE_SIZE, offset: this.offset }).then(data => {
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.total = data.total
this.offset += data.limit
if ($state) {
$state.loaded()
if (this.offset >= this.total) {
$state.complete()
}
}
},
play: function () {
@ -109,6 +103,19 @@ export default {
this.selected_track = track
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>

View File

@ -27,21 +27,21 @@
<!-- Tracks -->
<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>
</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">
<template slot="actions">
<template v-slot:actions>
<a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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" />
</template>
<template slot="footer">
<template v-slot:footer>
<nav v-if="show_all_tracks_button" class="level">
<p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a>
@ -50,28 +50,28 @@
</template>
</content-with-heading>
<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>
</template>
</content-text>
<!-- Artists -->
<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>
</template>
<template slot="content">
<template v-slot:content>
<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)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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" />
</template>
<template slot="footer">
<template v-slot:footer>
<nav v-if="show_all_artists_button" class="level">
<p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a>
@ -80,22 +80,22 @@
</template>
</content-with-heading>
<content-text v-if="show_artists && !artists.total">
<template slot="content">
<template v-slot:content>
<p><i>No artists found</i></p>
</template>
</content-text>
<!-- Albums -->
<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>
</template>
<template slot="content">
<template v-slot:content>
<spotify-list-item-album v-for="album in albums.items"
:key="album.id"
: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">
<cover-artwork
:artwork_url="artwork_url(album)"
@ -105,16 +105,16 @@
:maxheight="64" />
</p>
</template>
<template slot="actions">
<template v-slot:actions>
<a @click="open_album_dialog(album)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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" />
</template>
<template slot="footer">
<template v-slot:footer>
<nav v-if="show_all_albums_button" class="level">
<p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a>
@ -123,28 +123,28 @@
</template>
</content-with-heading>
<content-text v-if="show_albums && !albums.total">
<template slot="content">
<template v-slot:content>
<p><i>No albums found</i></p>
</template>
</content-text>
<!-- Playlists -->
<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>
</template>
<template slot="content">
<template v-slot:content>
<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)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</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" />
</template>
<template slot="footer">
<template v-slot:footer>
<nav v-if="show_all_playlists_button" class="level">
<p class="level-item">
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a>
@ -153,7 +153,7 @@
</template>
</content-with-heading>
<content-text v-if="show_playlists && !playlists.total">
<template slot="content">
<template v-slot:content>
<p><i>No playlists found</i></p>
</template>
</content-text>
@ -161,26 +161,28 @@
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading'
import ContentText from '@/templates/ContentText'
import TabsSearch from '@/components/TabsSearch'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack'
import SpotifyListItemArtist from '@/components/SpotifyListItemArtist'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
import CoverArtwork from '@/components/CoverArtwork'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ContentText from '@/templates/ContentText.vue'
import TabsSearch from '@/components/TabsSearch.vue'
import SpotifyListItemTrack from '@/components/SpotifyListItemTrack.vue'
import SpotifyListItemArtist from '@/components/SpotifyListItemArtist.vue'
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum.vue'
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist.vue'
import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack.vue'
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist.vue'
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum.vue'
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist.vue'
import CoverArtwork from '@/components/CoverArtwork.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import webapi from '@/webapi'
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 {
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 () {
return {
@ -266,7 +268,7 @@ export default {
}
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.$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.tracks.items = this.tracks.items.concat(data.tracks.items)
this.tracks.total = data.tracks.total
this.search_param.offset += data.tracks.limit
$state.loaded()
if (this.search_param.offset >= this.tracks.total) {
$state.complete()
}
loaded(data.tracks.items.length, PAGE_SIZE)
})
},
search_artists_next: function ($state) {
search_artists_next: function ({ loaded }) {
this.spotify_search().then(data => {
this.artists.items = this.artists.items.concat(data.artists.items)
this.artists.total = data.artists.total
this.search_param.offset += data.artists.limit
$state.loaded()
if (this.search_param.offset >= this.artists.total) {
$state.complete()
}
loaded(data.artists.items.length, PAGE_SIZE)
})
},
search_albums_next: function ($state) {
search_albums_next: function ({ loaded }) {
this.spotify_search().then(data => {
this.albums.items = this.albums.items.concat(data.albums.items)
this.albums.total = data.albums.total
this.search_param.offset += data.albums.limit
$state.loaded()
if (this.search_param.offset >= this.albums.total) {
$state.complete()
}
loaded(data.albums.items.length, PAGE_SIZE)
})
},
search_playlists_next: function ($state) {
search_playlists_next: function ({ loaded }) {
this.spotify_search().then(data => {
this.playlists.items = this.playlists.items.concat(data.playlists.items)
this.playlists.total = data.playlists.total
this.search_param.offset += data.playlists.limit
$state.loaded()
if (this.search_param.offset >= this.playlists.total) {
$state.complete()
}
loaded(data.playlists.items.length, PAGE_SIZE)
})
},

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 VueRouter from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import store from '@/store'
import * as types from '@/store/mutation_types'
import PageQueue from '@/pages/PageQueue'
import PageNowPlaying from '@/pages/PageNowPlaying'
import PageBrowse from '@/pages/PageBrowse'
import PageBrowseRecentlyAdded from '@/pages/PageBrowseRecentlyAdded'
import PageBrowseRecentlyPlayed from '@/pages/PageBrowseRecentlyPlayed'
import PageArtists from '@/pages/PageArtists'
import PageArtist from '@/pages/PageArtist'
import PageAlbums from '@/pages/PageAlbums'
import PageAlbum from '@/pages/PageAlbum'
import PageGenres from '@/pages/PageGenres'
import PageGenre from '@/pages/PageGenre'
import PageGenreTracks from '@/pages/PageGenreTracks'
import PageArtistTracks from '@/pages/PageArtistTracks'
import PageComposers from '@/pages/PageComposers'
import PageComposer from '@/pages/PageComposer'
import PageComposerTracks from '@/pages/PageComposerTracks'
import PagePodcasts from '@/pages/PagePodcasts'
import PagePodcast from '@/pages/PagePodcast'
import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums'
import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists'
import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist'
import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum'
import PagePlaylists from '@/pages/PagePlaylists'
import PagePlaylist from '@/pages/PagePlaylist'
import PageFiles from '@/pages/PageFiles'
import PageRadioStreams from '@/pages/PageRadioStreams'
import PageSearch from '@/pages/PageSearch'
import PageAbout from '@/pages/PageAbout'
import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse'
import SpotifyPageBrowseNewReleases from '@/pages/SpotifyPageBrowseNewReleases'
import SpotifyPageBrowseFeaturedPlaylists from '@/pages/SpotifyPageBrowseFeaturedPlaylists'
import SpotifyPageArtist from '@/pages/SpotifyPageArtist'
import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum'
import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist'
import SpotifyPageSearch from '@/pages/SpotifyPageSearch'
import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface'
import SettingsPageArtwork from '@/pages/SettingsPageArtwork'
import SettingsPageOnlineServices from '@/pages/SettingsPageOnlineServices'
import SettingsPageRemotesOutputs from '@/pages/SettingsPageRemotesOutputs'
import PageQueue from '@/pages/PageQueue.vue'
import PageNowPlaying from '@/pages/PageNowPlaying.vue'
import PageBrowse from '@/pages/PageBrowse.vue'
import PageBrowseRecentlyAdded from '@/pages/PageBrowseRecentlyAdded.vue'
import PageBrowseRecentlyPlayed from '@/pages/PageBrowseRecentlyPlayed.vue'
import PageArtists from '@/pages/PageArtists.vue'
import PageArtist from '@/pages/PageArtist.vue'
import PageAlbums from '@/pages/PageAlbums.vue'
import PageAlbum from '@/pages/PageAlbum.vue'
import PageGenres from '@/pages/PageGenres.vue'
import PageGenre from '@/pages/PageGenre.vue'
import PageGenreTracks from '@/pages/PageGenreTracks.vue'
import PageArtistTracks from '@/pages/PageArtistTracks.vue'
import PageComposers from '@/pages/PageComposers.vue'
import PageComposer from '@/pages/PageComposer.vue'
import PageComposerTracks from '@/pages/PageComposerTracks.vue'
import PagePodcasts from '@/pages/PagePodcasts.vue'
import PagePodcast from '@/pages/PagePodcast.vue'
import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums.vue'
import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists.vue'
import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist.vue'
import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum.vue'
import PagePlaylists from '@/pages/PagePlaylists.vue'
import PagePlaylist from '@/pages/PagePlaylist.vue'
import PageFiles from '@/pages/PageFiles.vue'
import PageRadioStreams from '@/pages/PageRadioStreams.vue'
import PageSearch from '@/pages/PageSearch.vue'
import PageAbout from '@/pages/PageAbout.vue'
import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse.vue'
import SpotifyPageBrowseNewReleases from '@/pages/SpotifyPageBrowseNewReleases.vue'
import SpotifyPageBrowseFeaturedPlaylists from '@/pages/SpotifyPageBrowseFeaturedPlaylists.vue'
import SpotifyPageArtist from '@/pages/SpotifyPageArtist.vue'
import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum.vue'
import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist.vue'
import SpotifyPageSearch from '@/pages/SpotifyPageSearch.vue'
import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface.vue'
import SettingsPageArtwork from '@/pages/SettingsPageArtwork.vue'
import SettingsPageOnlineServices from '@/pages/SettingsPageOnlineServices.vue'
import SettingsPageRemotesOutputs from '@/pages/SettingsPageRemotesOutputs.vue'
Vue.use(VueRouter)
export const router = new VueRouter({
export const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
@ -289,34 +287,47 @@ export const router = new VueRouter({
}
],
scrollBehavior (to, from, savedPosition) {
const wait_ms = 0
// console.log(to.path + '_' + from.path + '__' + to.hash + ' savedPosition:' + savedPosition)
if (savedPosition) {
// We have saved scroll position (browser back/forward navigation), use this position
return new Promise((resolve, reject) => {
setTimeout(() => {
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) => {
setTimeout(() => {
resolve({ selector: to.hash, offset: { x: 0, y: 120 } })
}, 10)
resolve({ el: to.hash, top: 120 })
}, 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) => {
setTimeout(() => {
if (to.meta.has_tabs) {
resolve({ selector: '#top', offset: { x: 0, y: 140 } })
resolve({ el: '#top', top: 140 })
} 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