[web] Migration to Vue 3 and Vite
|
@ -1,4 +0,0 @@
|
|||
> 0.25%
|
||||
not ie <= 8
|
||||
not dead
|
||||
not op_mini all
|
|
@ -1,2 +0,0 @@
|
|||
VUE_APP_JSON_API_SERVER='http://localhost:3689'
|
||||
VUE_APP_WEBSOCKET_SERVER='ws://localhost:3688'
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -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>
|
After Width: | Height: | Size: 875 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -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>
|
|
@ -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 |
After Width: | Height: | Size: 5.9 KiB |
|
@ -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 |
|
@ -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"
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'],
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import NavbarItemLink from './NavbarItemLink'
|
||||
import NavbarItemLink from './NavbarItemLink.vue'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 & 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 & 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>
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 }
|
|
@ -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/>'
|
||||
})
|
|
@ -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')
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 "now playing page"</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 "now playing page"</template>
|
||||
</settings-checkbox>
|
||||
<settings-textfield category_name="webinterface" option_name="show_composer_for_genre"
|
||||
:disabled="!settings_option_show_composer_now_playing"
|
||||
placeholder="Genres">
|
||||
<template slot="label">Show composer only for listed genres</template>
|
||||
<template slot="info">
|
||||
<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 "now playing page".
|
||||
</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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
})
|
|
@ -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 }
|
||||
}
|
||||
})
|
||||
|
||||
|
|