Merge pull request #1422 from chme/web/next
[web] Migration to Vue 3 and Vite
@ -1,6 +1,7 @@
|
||||
# OwnTone player web interface
|
||||
# OwnTone web interface
|
||||
|
||||
Mobile friendly player web interface for [OwnTone](http://owntone.github.io/owntone-server/) build with [Vue.js](https://vuejs.org), [Bulma](http://bulma.io).
|
||||
Mobile friendly player web interface for [OwnTone](http://owntone.github.io/owntone-server/) build
|
||||
with [Vue.js](https://vuejs.org), [Bulma](http://bulma.io).
|
||||
|
||||
## Screenshots
|
||||
|
||||
@ -21,20 +22,34 @@ The source is located in the `web-src` folder.
|
||||
cd web-src
|
||||
```
|
||||
|
||||
It is based on the Vue.js webpack template. For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
|
||||
The web interface is built with [Vite](https://vitejs.dev/), makes use of Prettier for code formatting
|
||||
and ESLint for code linting (the project was set up following the guide [ESLint and Prettier with Vite and Vue.js 3](https://vueschool.io/articles/vuejs-tutorials/eslint-and-prettier-with-vite-and-vue-js-3/)
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
# Serve with hot reload at localhost:3000
|
||||
# (assumes that OwnTone server is running on localhost:3689)
|
||||
npm run serve
|
||||
|
||||
# build for production with minification (will update player web interface in "../htdocs")
|
||||
# Serve with hot reload at localhost:3000
|
||||
# (with remote OwnTone server reachable under owntone.local:3689)
|
||||
VITE_OWNTONE_URL=http://owntone.local:3689 npm run serve
|
||||
|
||||
|
||||
# Build for production with minification (will update web interface in "../htdocs")
|
||||
npm run build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Lint code (and fix errors that can be automatically fixed)
|
||||
npm run lint
|
||||
```
|
||||
|
||||
After running `npm run dev` the web interface is reachable at [localhost:8080](http://localhost:8080). By default it expects **owntone** to be running at [localhost:3689](http://localhost:3689) and proxies all JSON API calls to this location. If the server is running at a different location you need to modify the `proxyTable` configuration in `config/index.js`
|
||||
After running `npm run serve` the web interface is reachable at [localhost:3000](http://localhost:3000).
|
||||
By default it expects **owntone** to be running at [localhost:3689](http://localhost:3689) and proxies all
|
||||
JSON API calls to this location.
|
||||
|
||||
If the server is running at a different location you have to set the env variable `VITE_OWNTONE_URL`.
|
||||
|
@ -20,36 +20,15 @@ dist_htdocs_DATA = \
|
||||
site.webmanifest
|
||||
|
||||
if COND_WEBINTERFACE
|
||||
htdocsplayercssdir = $(datadir)/owntone/htdocs/player/css
|
||||
htdocsassetsdir = $(datadir)/owntone/htdocs/assets
|
||||
|
||||
dist_htdocsplayercss_DATA = \
|
||||
player/css/app.css \
|
||||
player/css/app.css.map \
|
||||
player/css/chunk-vendors.css \
|
||||
player/css/chunk-vendors.css.map
|
||||
|
||||
htdocsplayerfontsdir = $(datadir)/owntone/htdocs/player/fonts
|
||||
|
||||
dist_htdocsplayerfonts_DATA = \
|
||||
player/fonts/materialdesignicons-webfont.ttf \
|
||||
player/fonts/materialdesignicons-webfont.woff2 \
|
||||
player/fonts/materialdesignicons-webfont.woff \
|
||||
player/fonts/materialdesignicons-webfont.eot
|
||||
|
||||
htdocsplayerjsdir = $(datadir)/owntone/htdocs/player/js
|
||||
|
||||
dist_htdocsplayerjs_DATA = \
|
||||
player/js/app.js \
|
||||
player/js/app.js.map \
|
||||
player/js/chunk-vendors.js \
|
||||
player/js/chunk-vendors.js.map \
|
||||
player/js/app-legacy.js \
|
||||
player/js/app-legacy.js.map \
|
||||
player/js/chunk-vendors-legacy.js \
|
||||
player/js/chunk-vendors-legacy.js.map
|
||||
|
||||
htdocsplayerimgdir = $(datadir)/owntone/htdocs/player/img
|
||||
|
||||
dist_htdocsplayerimg_DATA = \
|
||||
player/img/materialdesignicons-webfont.svg
|
||||
dist_htdocsassets_DATA = \
|
||||
assets/index.css \
|
||||
assets/index.js \
|
||||
assets/vendor.js \
|
||||
assets/materialdesignicons-webfont.svg \
|
||||
assets/materialdesignicons-webfont.ttf \
|
||||
assets/materialdesignicons-webfont.woff2 \
|
||||
assets/materialdesignicons-webfont.woff \
|
||||
assets/materialdesignicons-webfont.eot
|
||||
endif
|
||||
|
1
htdocs/assets/index.css
Normal file
1
htdocs/assets/index.js
Normal file
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
56
htdocs/assets/vendor.js
Normal file
@ -1 +1,27 @@
|
||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>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"><link href="player/css/app.css" rel="preload" as="style"><link href="player/css/chunk-vendors.css" rel="preload" as="style"><link href="player/js/app.js" rel="modulepreload" as="script"><link href="player/js/chunk-vendors.js" rel="modulepreload" as="script"><link href="player/css/chunk-vendors.css" rel="stylesheet"><link href="player/css/app.css" rel="stylesheet"></head><body><div id="app"></div><script type="module" src="player/js/chunk-vendors.js"></script><script type="module" src="player/js/app.js"></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src="player/js/chunk-vendors-legacy.js" nomodule></script><script src="player/js/app-legacy.js" nomodule></script></body></html>
|
||||
<!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?ver=2.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>
|
||||
<script type="module" crossorigin src="/assets/index.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.js">
|
||||
<link rel="stylesheet" href="/assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "OwnTone",
|
||||
"short_name": "OwnTone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "OwnTone",
|
||||
"short_name": "OwnTone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
@ -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 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'vue/require-prop-types': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/prop-name-casing': ['warn', 'snake_case']
|
||||
}
|
||||
}
|
||||
|
5
web-src/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
24
web-src/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!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?ver=2.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>
|
3
web-src/jsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"include": ["./src/**/*"]
|
||||
}
|
35767
web-src/package-lock.json
generated
@ -1,54 +1,42 @@
|
||||
{
|
||||
"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",
|
||||
"serve": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.25.0",
|
||||
"@aacassandra/vue3-progressbar": "^1.0.3",
|
||||
"@ts-pro/vue-eternal-loading": "^1.2.0",
|
||||
"@vueform/slider": "^2.0.9",
|
||||
"axios": "^0.26.1",
|
||||
"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.14",
|
||||
"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.4",
|
||||
"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.4",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"prettier": "2.6.0",
|
||||
"sass": "^1.49.9",
|
||||
"vite": "^2.8.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
BIN
web-src/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
web-src/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
web-src/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
9
web-src/public/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
web-src/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 875 B |
BIN
web-src/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web-src/public/favicon.ico
Normal file
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>
|
31
web-src/public/logo.svg
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#107A66;}
|
||||
.st1{fill:#479386;}
|
||||
.st2{fill:#00D1B2;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M390.5,193.9h30.3c18.5,49.6,13.9,104.8-12.7,150.6c-5.1,8.8-11,17.2-17.6,24.9V193.9z"/>
|
||||
<path class="st1" d="M440.3,163.9h-79.8v269.6C453.8,378.5,488.6,260.8,440.3,163.9L440.3,163.9z"/>
|
||||
<path class="st2" d="M256,432c-97,0-176-79-176-176S159,80,256,80c26.9-0.1,53.4,6.1,77.5,17.9H187.9v96h99.8v235.2
|
||||
C277.2,431,266.6,432,256,432z"/>
|
||||
<path class="st1" d="M157.9,147.8v76.1h99.8v178H256c-80.6,0-146-65.4-145.9-146.1c0-38.7,15.4-75.8,42.7-103.1
|
||||
C154.4,151,156.1,149.4,157.9,147.8 M256,50C142.2,50,50,142.2,50,256s92.2,206,206,206c20.9,0,41.7-3.2,61.7-9.4V163.9h-99.8v-36
|
||||
h199.4C378.3,78.6,318.8,49.9,256,50z"/>
|
||||
<path class="st2" d="M429.7,183.5c19.7,46.8,19.7,99.6,0,146.4c-12.9,30.1-33.2,56.4-59.2,76.3v3.8c48.5-36,77-92.9,77-153.3
|
||||
c0.1-28.4-6.3-56.4-18.5-82h-3.3C427.1,177.6,428.5,180.5,429.7,183.5z"/>
|
||||
<path class="st2" d="M434,256.7c0.1-28.5-6.8-56.7-20-82h-3.4c2.4,4.6,4.7,9.3,6.7,14.1c18.3,43.4,18.3,92.4,0,135.8
|
||||
c-8.8,20.7-21.5,39.6-37.4,55.5c-3,3-6.2,5.9-9.4,8.7v3.9C410.8,359,434,309.2,434,256.7z"/>
|
||||
<path class="st2" d="M420.5,256.7c0-28.8-7.5-57.1-21.9-82h-3.5c3.7,6.2,6.9,12.7,9.8,19.3c25.6,60.3,11.9,130.1-34.4,176.4v4.2
|
||||
C402.5,343.7,420.5,301.2,420.5,256.7z"/>
|
||||
<path class="st2" d="M382.7,174.7h-3.5c5.2,7.8,9.6,16,13.2,24.6c21.3,50.5,12.9,108.7-21.9,151v4.7
|
||||
C414.2,304.3,419.1,230.8,382.7,174.7z"/>
|
||||
<path class="st2" d="M370.5,180.7v5.5c26.7,43.2,26.7,97.8,0,141v5.5C401.1,286.7,401.1,226.7,370.5,180.7z"/>
|
||||
<g>
|
||||
<path class="st3" d="M417.3,127.9H217.9v36h99.8v288.7c15.1-4.7,29.5-11.2,42.8-19.1V163.9h79.8C433.9,151.1,426.2,139,417.3,127.9
|
||||
z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
web-src/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
42
web-src/public/safari-pinned-tab.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M3173 6294 c-330 -40 -614 -124 -918 -274 -299 -147 -489 -284 -741
|
||||
-535 -250 -249 -386 -439 -540 -750 -128 -260 -212 -532 -261 -845 -26 -164
|
||||
-26 -616 0 -780 87 -555 303 -1035 647 -1435 450 -523 1045 -852 1740 -960
|
||||
141 -22 515 -31 662 -16 159 17 324 46 461 83 l117 31 0 1973 0 1974 -680 0
|
||||
-680 0 0 250 0 250 1356 0 1355 0 -22 33 c-12 17 -97 106 -188 197 -250 249
|
||||
-439 385 -746 535 -310 152 -615 240 -954 274 -140 15 -470 12 -608 -5z m-333
|
||||
-2354 l680 0 0 -1215 0 -1215 -79 0 c-261 0 -571 79 -836 212 -187 94 -331
|
||||
197 -488 347 -391 375 -607 885 -607 1431 0 531 200 1020 572 1400 l73 75 3
|
||||
-517 2 -518 680 0z"/>
|
||||
<path d="M4930 2920 l0 -1841 23 12 c43 23 193 127 287 199 136 104 389 362
|
||||
496 505 298 398 476 825 550 1317 21 145 30 505 15 654 -32 315 -117 629 -244
|
||||
907 l-40 87 -543 0 -544 0 0 -1840z m318 1662 c38 -53 131 -237 168 -332 90
|
||||
-231 133 -441 141 -686 9 -264 -21 -479 -103 -724 -74 -222 -168 -402 -313
|
||||
-594 -60 -80 -66 -86 -69 -61 -3 18 8 41 36 78 518 678 555 1608 94 2325 -13
|
||||
20 -13 22 6 22 11 0 30 -13 40 -28z m234 -34 c134 -260 208 -493 249 -788 18
|
||||
-128 15 -432 -6 -575 -67 -464 -269 -887 -584 -1225 -63 -67 -70 -72 -71 -50
|
||||
0 18 21 50 71 105 370 411 569 927 569 1473 0 343 -73 659 -223 962 -39 80
|
||||
-73 148 -75 153 -2 4 6 7 17 7 15 0 29 -16 53 -62z m234 -58 c89 -197 151
|
||||
-405 191 -645 25 -153 25 -542 0 -700 -87 -556 -349 -1057 -743 -1423 -74 -69
|
||||
-90 -79 -92 -63 -2 15 27 51 105 128 366 365 593 798 684 1308 27 150 37 491
|
||||
20 646 -32 283 -110 556 -222 781 -22 43 -39 80 -39 83 0 4 10 5 22 3 18 -2
|
||||
33 -26 74 -118z m193 18 c382 -906 225 -1946 -407 -2698 -91 -109 -242 -257
|
||||
-354 -348 -71 -58 -78 -61 -78 -40 0 18 27 47 112 119 177 150 371 375 505
|
||||
586 85 136 207 394 257 543 49 150 86 307 113 480 23 154 23 527 0 683 -36
|
||||
236 -101 471 -183 663 -24 57 -44 106 -44 109 0 3 8 5 18 5 13 0 29 -27 61
|
||||
-102z m-775 -95 c107 -189 188 -424 222 -639 25 -165 23 -424 -5 -588 -40
|
||||
-231 -111 -430 -219 -620 -50 -85 -57 -95 -60 -71 -3 19 16 65 56 144 110 212
|
||||
168 393 197 617 49 376 -30 795 -211 1118 -24 43 -44 91 -44 107 0 16 2 29 4
|
||||
29 3 0 30 -44 60 -97z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
19
web-src/public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "OwnTone",
|
||||
"short_name": "OwnTone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
@ -2,27 +2,34 @@
|
||||
<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>
|
||||
<modal-dialog-remote-pairing :show="pairing_active" @close="pairing_active = false" />
|
||||
<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"
|
||||
@close="show_update_dialog = false" />
|
||||
<notifications v-show="!show_burger_menu" />
|
||||
:show="show_update_dialog"
|
||||
@close="show_update_dialog = false"
|
||||
/>
|
||||
<notification-list v-show="!show_burger_menu" />
|
||||
<navbar-bottom />
|
||||
<div class="fd-overlay-fullscreen" v-show="show_burger_menu || show_player_menu"
|
||||
@click="show_burger_menu = show_player_menu = false"></div>
|
||||
<div
|
||||
v-show="show_burger_menu || show_player_menu"
|
||||
class="fd-overlay-fullscreen"
|
||||
@click="show_burger_menu = show_player_menu = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 NotificationList from '@/components/NotificationList.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'
|
||||
@ -30,10 +37,15 @@ import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { NavbarTop, NavbarBottom, Notifications, ModalDialogRemotePairing, ModalDialogUpdate },
|
||||
template: '<App/>',
|
||||
components: {
|
||||
NavbarTop,
|
||||
NavbarBottom,
|
||||
NotificationList,
|
||||
ModalDialogRemotePairing,
|
||||
ModalDialogUpdate
|
||||
},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
token_timer_id: 0,
|
||||
reconnect_attempts: 0,
|
||||
@ -43,31 +55,40 @@ export default {
|
||||
|
||||
computed: {
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
},
|
||||
show_player_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
},
|
||||
show_update_dialog: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_update_dialog
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
show_burger_menu() {
|
||||
this.update_is_clipped()
|
||||
},
|
||||
show_player_menu() {
|
||||
this.update_is_clipped()
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
moment.locale(navigator.language)
|
||||
this.connect()
|
||||
@ -97,23 +118,40 @@ export default {
|
||||
|
||||
methods: {
|
||||
connect: function () {
|
||||
this.$store.dispatch('add_notification', { text: 'Connecting to OwnTone server', type: 'info', topic: 'connection', timeout: 2000 })
|
||||
|
||||
webapi.config().then(({ data }) => {
|
||||
this.$store.commit(types.UPDATE_CONFIG, data)
|
||||
this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
|
||||
document.title = data.library_name
|
||||
|
||||
this.open_ws()
|
||||
this.$Progress.finish()
|
||||
}).catch(() => {
|
||||
this.$store.dispatch('add_notification', { text: 'Failed to connect to OwnTone server', type: 'danger', topic: 'connection' })
|
||||
/*
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'Connecting to OwnTone server',
|
||||
type: 'info',
|
||||
topic: 'connection',
|
||||
timeout: 2000
|
||||
})
|
||||
*/
|
||||
|
||||
webapi
|
||||
.config()
|
||||
.then(({ data }) => {
|
||||
this.$store.commit(types.UPDATE_CONFIG, data)
|
||||
this.$store.commit(types.HIDE_SINGLES, data.hide_singles)
|
||||
document.title = data.library_name
|
||||
|
||||
this.open_ws()
|
||||
this.$Progress.finish()
|
||||
})
|
||||
.catch(() => {
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'Failed to connect to OwnTone server',
|
||||
type: 'danger',
|
||||
topic: 'connection'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
open_ws: function () {
|
||||
if (this.$store.state.config.websocket_port <= 0) {
|
||||
this.$store.dispatch('add_notification', { text: 'Missing websocket port', type: 'danger' })
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'Missing websocket port',
|
||||
type: 'danger'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -124,22 +162,54 @@ export default {
|
||||
protocol = 'wss://'
|
||||
}
|
||||
|
||||
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 we are running in the development server, use the websocket url configured in .env.development
|
||||
wsUrl = process.env.VUE_APP_WEBSOCKET_SERVER
|
||||
let wsUrl =
|
||||
protocol +
|
||||
window.location.hostname +
|
||||
':' +
|
||||
vm.$store.state.config.websocket_port
|
||||
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_OWNTONE_URL) {
|
||||
// If we are running in development mode, construct the websocket url
|
||||
// from the host of the environment variable VITE_OWNTONE_URL
|
||||
const owntoneUrl = new URL(import.meta.env.VITE_OWNTONE_URL)
|
||||
wsUrl =
|
||||
protocol +
|
||||
owntoneUrl.hostname +
|
||||
':' +
|
||||
vm.$store.state.config.websocket_port
|
||||
}
|
||||
|
||||
const socket = new ReconnectingWebSocket(
|
||||
wsUrl,
|
||||
'notify',
|
||||
{ reconnectInterval: 3000 }
|
||||
)
|
||||
const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
|
||||
reconnectInterval: 1000,
|
||||
maxReconnectInterval: 2000
|
||||
})
|
||||
|
||||
socket.onopen = function () {
|
||||
vm.$store.dispatch('add_notification', { text: 'Connection to server established', type: 'primary', topic: 'connection', timeout: 2000 })
|
||||
/*
|
||||
vm.$store.dispatch('add_notification', {
|
||||
text: 'Connection to server established',
|
||||
type: 'primary',
|
||||
topic: 'connection',
|
||||
timeout: 2000
|
||||
})
|
||||
*/
|
||||
vm.reconnect_attempts = 0
|
||||
socket.send(JSON.stringify({ notify: ['update', 'database', 'player', 'options', 'outputs', 'volume', 'queue', 'spotify', 'lastfm', 'pairing'] }))
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
notify: [
|
||||
'update',
|
||||
'database',
|
||||
'player',
|
||||
'options',
|
||||
'outputs',
|
||||
'volume',
|
||||
'queue',
|
||||
'spotify',
|
||||
'lastfm',
|
||||
'pairing'
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
vm.update_outputs()
|
||||
vm.update_player_status()
|
||||
@ -153,16 +223,66 @@ export default {
|
||||
socket.onclose = function () {
|
||||
// vm.$store.dispatch('add_notification', { text: 'Connection closed', type: 'danger', timeout: 2000 })
|
||||
}
|
||||
/*
|
||||
socket.onerror = function () {
|
||||
vm.reconnect_attempts++
|
||||
vm.$store.dispatch('add_notification', { text: 'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')', type: 'danger', topic: 'connection' })
|
||||
vm.$store.dispatch('add_notification', {
|
||||
text:
|
||||
'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')',
|
||||
type: 'danger',
|
||||
topic: 'connection'
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
// When the app becomes active, force an update of all information, because we
|
||||
// may have missed notifications while the app was inactive.
|
||||
// There are two relevant events (focus and visibilitychange), so we throttle
|
||||
// the updates to avoid multiple redundant updates
|
||||
var update_throttled = false
|
||||
|
||||
function update_info() {
|
||||
if (update_throttled) {
|
||||
return
|
||||
}
|
||||
|
||||
vm.update_outputs()
|
||||
vm.update_player_status()
|
||||
vm.update_library_stats()
|
||||
vm.update_settings()
|
||||
vm.update_queue()
|
||||
vm.update_spotify()
|
||||
vm.update_lastfm()
|
||||
vm.update_pairing()
|
||||
|
||||
update_throttled = true
|
||||
setTimeout(function () {
|
||||
update_throttled = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// These events are fired when the window becomes active in different ways
|
||||
// When this happens, we should update 'now playing' info etc
|
||||
window.addEventListener('focus', update_info)
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') {
|
||||
update_info()
|
||||
}
|
||||
})
|
||||
|
||||
socket.onmessage = function (response) {
|
||||
const data = JSON.parse(response.data)
|
||||
if (data.notify.includes('update') || data.notify.includes('database')) {
|
||||
if (
|
||||
data.notify.includes('update') ||
|
||||
data.notify.includes('database')
|
||||
) {
|
||||
vm.update_library_stats()
|
||||
}
|
||||
if (data.notify.includes('player') || data.notify.includes('options') || data.notify.includes('volume')) {
|
||||
if (
|
||||
data.notify.includes('player') ||
|
||||
data.notify.includes('options') ||
|
||||
data.notify.includes('volume')
|
||||
) {
|
||||
vm.update_player_status()
|
||||
}
|
||||
if (data.notify.includes('outputs') || data.notify.includes('volume')) {
|
||||
@ -237,7 +357,10 @@ export default {
|
||||
this.token_timer_id = 0
|
||||
}
|
||||
if (data.webapi_token_expires_in > 0 && data.webapi_token) {
|
||||
this.token_timer_id = window.setTimeout(this.update_spotify, 1000 * data.webapi_token_expires_in)
|
||||
this.token_timer_id = window.setTimeout(
|
||||
this.update_spotify,
|
||||
1000 * data.webapi_token_expires_in
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -257,17 +380,8 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show_burger_menu' () {
|
||||
this.update_is_clipped()
|
||||
},
|
||||
'show_player_menu' () {
|
||||
this.update_is_clipped()
|
||||
}
|
||||
}
|
||||
template: '<App/>'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
_gain: null,
|
||||
|
||||
// setup audio routing
|
||||
setupAudio () {
|
||||
setupAudio() {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||
this._context = new AudioContext()
|
||||
this._source = this._context.createMediaElementSource(this._audio)
|
||||
@ -18,26 +18,26 @@ export default {
|
||||
this._source.connect(this._gain)
|
||||
this._gain.connect(this._context.destination)
|
||||
|
||||
this._audio.addEventListener('canplaythrough', e => {
|
||||
this._audio.addEventListener('canplaythrough', (e) => {
|
||||
this._audio.play()
|
||||
})
|
||||
this._audio.addEventListener('canplay', e => {
|
||||
this._audio.addEventListener('canplay', (e) => {
|
||||
this._audio.play()
|
||||
})
|
||||
return this._audio
|
||||
},
|
||||
|
||||
// set audio volume
|
||||
setVolume (volume) {
|
||||
setVolume(volume) {
|
||||
if (!this._gain) return
|
||||
volume = parseFloat(volume) || 0.0
|
||||
volume = (volume < 0) ? 0 : volume
|
||||
volume = (volume > 1) ? 1 : volume
|
||||
volume = volume < 0 ? 0 : volume
|
||||
volume = volume > 1 ? 1 : volume
|
||||
this._gain.gain.value = volume
|
||||
},
|
||||
|
||||
// play audio source url
|
||||
playSource (source) {
|
||||
playSource(source) {
|
||||
this.stopAudio()
|
||||
this._context.resume().then(() => {
|
||||
this._audio.src = String(source || '') + '?x=' + Date.now()
|
||||
@ -47,9 +47,21 @@ export default {
|
||||
},
|
||||
|
||||
// stop playing audio
|
||||
stopAudio () {
|
||||
try { this._audio.pause() } catch (e) {}
|
||||
try { this._audio.stop() } catch (e) {}
|
||||
try { this._audio.close() } catch (e) {}
|
||||
stopAudio() {
|
||||
try {
|
||||
this._audio.pause()
|
||||
} catch (e) {
|
||||
// continue regardless of error
|
||||
}
|
||||
try {
|
||||
this._audio.stop()
|
||||
} catch (e) {
|
||||
// continue regardless of error
|
||||
}
|
||||
try {
|
||||
this._audio.close()
|
||||
} catch (e) {
|
||||
// continue regardless of error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,53 @@
|
||||
<template>
|
||||
<figure>
|
||||
<img v-lazyload
|
||||
:data-src="artwork_url_with_size"
|
||||
:data-err="dataURI"
|
||||
:key="artwork_url_with_size"
|
||||
@click="$emit('click')">
|
||||
<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',
|
||||
props: ['artist', 'album', 'artwork_url', 'maxwidth', 'maxheight'],
|
||||
emits: ['click'],
|
||||
|
||||
data () {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
artwork_url_with_size: function () {
|
||||
if (this.maxwidth > 0 && this.maxheight > 0) {
|
||||
return webapi.artwork_url_append_size_params(this.artwork_url, this.maxwidth, this.maxheight)
|
||||
return webapi.artwork_url_append_size_params(
|
||||
this.artwork_url,
|
||||
this.maxwidth,
|
||||
this.maxheight
|
||||
)
|
||||
}
|
||||
return webapi.artwork_url_append_size_params(this.artwork_url)
|
||||
},
|
||||
|
||||
alt_text () {
|
||||
alt_text() {
|
||||
return this.artist + ' - ' + this.album
|
||||
},
|
||||
|
||||
caption () {
|
||||
caption() {
|
||||
if (this.album) {
|
||||
return this.album.substring(0, 2)
|
||||
}
|
||||
@ -48,47 +55,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,19 +1,31 @@
|
||||
<template>
|
||||
<div class="dropdown" :class="{ 'is-active': is_active }" v-click-outside="onClickOutside">
|
||||
<div
|
||||
v-click-away="onClickOutside"
|
||||
class="dropdown"
|
||||
:class="{ 'is-active': is_active }"
|
||||
>
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" @click="is_active = !is_active">
|
||||
<span>{{ value }}</span>
|
||||
<button
|
||||
class="button"
|
||||
aria-haspopup="true"
|
||||
aria-controls="dropdown-menu"
|
||||
@click="is_active = !is_active"
|
||||
>
|
||||
<span>{{ modelValue }}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="mdi mdi-chevron-down" aria-hidden="true"></i>
|
||||
<i class="mdi mdi-chevron-down" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div id="dropdown-menu" class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item"
|
||||
v-for="option in options" :key="option"
|
||||
:class="{'is-active': value === option}"
|
||||
@click="select(option)">
|
||||
<a
|
||||
v-for="option in options"
|
||||
:key="option"
|
||||
class="dropdown-item"
|
||||
:class="{ 'is-active': modelValue === option }"
|
||||
@click="select(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</a>
|
||||
</div>
|
||||
@ -25,26 +37,27 @@
|
||||
export default {
|
||||
name: 'DropdownMenu',
|
||||
|
||||
props: ['value', 'options'],
|
||||
// eslint-disable-next-line vue/prop-name-casing
|
||||
props: ['modelValue', 'options'],
|
||||
emits: ['update:modelValue'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
is_active: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickOutside (event) {
|
||||
onClickOutside(event) {
|
||||
this.is_active = false
|
||||
},
|
||||
|
||||
select (option) {
|
||||
select(option) {
|
||||
this.is_active = false
|
||||
this.$emit('input', option)
|
||||
this.$emit('update:modelValue', option)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<section>
|
||||
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 16px;">
|
||||
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a>
|
||||
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 16px">
|
||||
<a
|
||||
v-for="char in filtered_index"
|
||||
:key="char"
|
||||
class="button is-small"
|
||||
@click="nav(char)"
|
||||
>{{ char }}</a
|
||||
>
|
||||
</nav>
|
||||
</section>
|
||||
</template>
|
||||
@ -13,15 +19,18 @@ export default {
|
||||
props: ['index'],
|
||||
|
||||
computed: {
|
||||
filtered_index () {
|
||||
filtered_index() {
|
||||
if (!this.index) {
|
||||
return []
|
||||
}
|
||||
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
|
||||
return this.index.filter(c => !specialChars.includes(c))
|
||||
return this.index.filter((c) => !specialChars.includes(c))
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
nav: function (id) {
|
||||
this.$router.push({ path: this.$router.currentRoute.path + '#index_' + id })
|
||||
this.$router.push({ hash: '#index_' + id })
|
||||
},
|
||||
|
||||
scroll_to_top: function () {
|
||||
@ -31,5 +40,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,115 +1,129 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="is_grouped">
|
||||
<div v-for="idx in albums.indexList" :key="idx" class="mb-6">
|
||||
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
|
||||
<list-item-album 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">
|
||||
<cover-artwork
|
||||
:artwork_url="album.artwork_url"
|
||||
:artist="album.artist"
|
||||
:album="album.name"
|
||||
:maxwidth="64"
|
||||
:maxheight="64" />
|
||||
</p>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
</list-item-album>
|
||||
<template v-for="album in albums" :key="album.itemId">
|
||||
<div v-if="!album.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
|
||||
<span
|
||||
:id="'index_' + album.groupKey"
|
||||
class="tag is-info is-light is-small has-text-weight-bold"
|
||||
>{{ album.groupKey }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-else-if="album.isItem" class="media" @click="open_album(album.item)">
|
||||
<div v-if="is_visible_artwork" class="media-left fd-has-action">
|
||||
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||
<figure>
|
||||
<img
|
||||
v-lazy="{
|
||||
src: artwork_url_with_size(album.item.artwork_url),
|
||||
lifecycle: artwork_options.lazy_lifecycle
|
||||
}"
|
||||
:album="album.item.name"
|
||||
:artist="album.item.artist"
|
||||
/>
|
||||
</figure>
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<div style="margin-top: 0.7rem">
|
||||
<h1 class="title is-6">
|
||||
{{ album.item.name }}
|
||||
</h1>
|
||||
<h2 class="subtitle is-7 has-text-grey">
|
||||
<b>{{ album.item.artist }}</b>
|
||||
</h2>
|
||||
<h2
|
||||
v-if="album.item.date_released && album.item.media_kind === 'music'"
|
||||
class="subtitle is-7 has-text-grey has-text-weight-normal"
|
||||
>
|
||||
{{ $filters.time(album.item.date_released, 'L') }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-right" style="padding-top: 0.7rem">
|
||||
<a @click.prevent.stop="open_dialog(album.item)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<list-item-album v-for="album in albums_list"
|
||||
: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">
|
||||
<cover-artwork
|
||||
:artwork_url="album.artwork_url"
|
||||
:artist="album.artist"
|
||||
:album="album.name"
|
||||
:maxwidth="64"
|
||||
:maxheight="64" />
|
||||
</p>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
</list-item-album>
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-album
|
||||
:show="show_details_modal"
|
||||
:album="selected_album"
|
||||
:media_kind="media_kind"
|
||||
@remove-podcast="open_remove_podcast_dialog()"
|
||||
@play-count-changed="play_count_changed()"
|
||||
@close="show_details_modal = false" />
|
||||
:show="show_details_modal"
|
||||
:album="selected_album"
|
||||
:media_kind="media_kind"
|
||||
@remove-podcast="open_remove_podcast_dialog()"
|
||||
@play-count-changed="play_count_changed()"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
<modal-dialog
|
||||
:show="show_remove_podcast_modal"
|
||||
title="Remove podcast"
|
||||
delete_action="Remove"
|
||||
@close="show_remove_podcast_modal = false"
|
||||
@delete="remove_podcast">
|
||||
<template slot="modal-content">
|
||||
:show="show_remove_podcast_modal"
|
||||
title="Remove podcast"
|
||||
delete_action="Remove"
|
||||
@close="show_remove_podcast_modal = false"
|
||||
@delete="remove_podcast"
|
||||
>
|
||||
<template #modal-content>
|
||||
<p>Permanently remove this podcast from your library?</p>
|
||||
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p>
|
||||
<p class="is-size-7">
|
||||
(This will also remove the RSS playlist
|
||||
<b>{{ rss_playlist_to_remove.name }}</b
|
||||
>.)
|
||||
</p>
|
||||
</template>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialog from '@/components/ModalDialog'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
|
||||
import ModalDialog from '@/components/ModalDialog.vue'
|
||||
import webapi from '@/webapi'
|
||||
import Albums from '@/lib/Albums'
|
||||
import { renderSVG } from '@/lib/SVGRenderer'
|
||||
|
||||
export default {
|
||||
name: 'ListAlbums',
|
||||
components: { ListItemAlbum, ModalDialogAlbum, ModalDialog, CoverArtwork },
|
||||
components: { ModalDialogAlbum, ModalDialog },
|
||||
|
||||
props: ['albums', 'media_kind'],
|
||||
props: ['albums', 'media_kind', 'hide_group_title'],
|
||||
emits: ['play-count-changed', 'podcast-deleted'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
show_remove_podcast_modal: false,
|
||||
rss_playlist_to_remove: {}
|
||||
rss_playlist_to_remove: {},
|
||||
|
||||
artwork_options: {
|
||||
width: 600,
|
||||
height: 600,
|
||||
font_family: 'sans-serif',
|
||||
font_size: 200,
|
||||
font_weight: 600,
|
||||
lazy_lifecycle: {
|
||||
error: (el) => {
|
||||
el.src = this.dataURI(
|
||||
el.attributes.album.value,
|
||||
el.attributes.artist.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_visible_artwork () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||
is_visible_artwork() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_cover_artwork_in_album_lists'
|
||||
).value
|
||||
},
|
||||
|
||||
media_kind_resolved: function () {
|
||||
return this.media_kind ? this.media_kind : this.selected_album.media_kind
|
||||
},
|
||||
|
||||
albums_list: function () {
|
||||
if (Array.isArray(this.albums)) {
|
||||
return this.albums
|
||||
}
|
||||
return this.albums.sortedAndFiltered
|
||||
},
|
||||
|
||||
is_grouped: function () {
|
||||
return (this.albums instanceof Albums && this.albums.options.group)
|
||||
}
|
||||
},
|
||||
|
||||
@ -131,19 +145,24 @@ export default {
|
||||
},
|
||||
|
||||
open_remove_podcast_dialog: function () {
|
||||
webapi.library_album_tracks(this.selected_album.id, { limit: 1 }).then(({ data }) => {
|
||||
webapi.library_track_playlists(data.items[0].id).then(({ data }) => {
|
||||
const rssPlaylists = data.items.filter(pl => pl.type === 'rss')
|
||||
if (rssPlaylists.length !== 1) {
|
||||
this.$store.dispatch('add_notification', { text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.', type: 'danger' })
|
||||
return
|
||||
}
|
||||
webapi
|
||||
.library_album_tracks(this.selected_album.id, { limit: 1 })
|
||||
.then(({ data }) => {
|
||||
webapi.library_track_playlists(data.items[0].id).then(({ data }) => {
|
||||
const rssPlaylists = data.items.filter((pl) => pl.type === 'rss')
|
||||
if (rssPlaylists.length !== 1) {
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.',
|
||||
type: 'danger'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.rss_playlist_to_remove = rssPlaylists[0]
|
||||
this.show_remove_podcast_modal = true
|
||||
this.show_details_modal = false
|
||||
this.rss_playlist_to_remove = rssPlaylists[0]
|
||||
this.show_remove_podcast_modal = true
|
||||
this.show_details_modal = false
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
play_count_changed: function () {
|
||||
@ -152,13 +171,51 @@ export default {
|
||||
|
||||
remove_podcast: function () {
|
||||
this.show_remove_podcast_modal = false
|
||||
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => {
|
||||
this.$emit('podcast-deleted')
|
||||
webapi
|
||||
.library_playlist_delete(this.rss_playlist_to_remove.id)
|
||||
.then(() => {
|
||||
this.$emit('podcast-deleted')
|
||||
})
|
||||
},
|
||||
|
||||
artwork_url_with_size: function (artwork_url) {
|
||||
if (this.artwork_options.width > 0 && this.artwork_options.height > 0) {
|
||||
return webapi.artwork_url_append_size_params(
|
||||
artwork_url,
|
||||
this.artwork_options.width,
|
||||
this.artwork_options.height
|
||||
)
|
||||
}
|
||||
return webapi.artwork_url_append_size_params(artwork_url)
|
||||
},
|
||||
|
||||
alt_text(album, artist) {
|
||||
return artist + ' - ' + album
|
||||
},
|
||||
|
||||
caption(album, artist) {
|
||||
if (album) {
|
||||
return album.substring(0, 2)
|
||||
}
|
||||
if (artist) {
|
||||
return artist.substring(0, 2)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
dataURI: function (album, artist) {
|
||||
const caption = this.caption(album, artist)
|
||||
const alt_text = this.alt_text(album, artist)
|
||||
return renderSVG(caption, alt_text, {
|
||||
width: this.artwork_options.width,
|
||||
height: this.artwork_options.height,
|
||||
font_family: this.artwork_options.font_family,
|
||||
font_size: this.artwork_options.font_size,
|
||||
font_weight: this.artwork_options.font_weight
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,48 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="is_grouped">
|
||||
<div v-for="idx in artists.indexList" :key="idx" class="mb-6">
|
||||
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
|
||||
<list-item-artist v-for="artist in artists.grouped[idx]"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
@click="open_artist(artist)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(artist)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-artist>
|
||||
<template v-for="artist in artists" :key="artist.itemId">
|
||||
<div v-if="!artist.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
|
||||
<div class="media-content is-clipped">
|
||||
<span
|
||||
:id="'index_' + artist.groupKey"
|
||||
class="tag is-info is-light is-small has-text-weight-bold"
|
||||
>{{ artist.groupKey }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<list-item-artist v-for="artist in artists_list"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
@click="open_artist(artist)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(artist)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-artist>
|
||||
<div
|
||||
v-else-if="artist.isItem"
|
||||
class="media"
|
||||
@click="open_artist(artist.item)"
|
||||
>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">
|
||||
{{ artist.item.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(artist.item)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" :media_kind="media_kind" @close="show_details_modal = false" />
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-artist
|
||||
:show="show_details_modal"
|
||||
:artist="selected_artist"
|
||||
:media_kind="media_kind"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemArtist from '@/components/ListItemArtist'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import Artists from '@/lib/Artists'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
|
||||
|
||||
export default {
|
||||
name: 'ListArtists',
|
||||
components: { ListItemArtist, ModalDialogArtist },
|
||||
components: { ModalDialogArtist },
|
||||
|
||||
props: ['artists', 'media_kind'],
|
||||
props: ['artists', 'media_kind', 'hide_group_title'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_artist: {}
|
||||
@ -52,17 +57,6 @@ export default {
|
||||
computed: {
|
||||
media_kind_resolved: function () {
|
||||
return this.media_kind ? this.media_kind : this.selected_artist.media_kind
|
||||
},
|
||||
|
||||
artists_list: function () {
|
||||
if (Array.isArray(this.artists)) {
|
||||
return this.artists
|
||||
}
|
||||
return this.artists.sortedAndFiltered
|
||||
},
|
||||
|
||||
is_grouped: function () {
|
||||
return (this.artists instanceof Artists && this.artists.options.group)
|
||||
}
|
||||
},
|
||||
|
||||
@ -86,5 +80,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,48 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="is_grouped">
|
||||
<div v-for="idx in composers.indexList" :key="idx" class="mb-6">
|
||||
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
|
||||
<list-item-composer v-for="composer in composers.grouped[idx]"
|
||||
:key="composer.id"
|
||||
:composer="composer"
|
||||
@click="open_composer(composer)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(composer)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-composer>
|
||||
<template v-for="composer in composers" :key="composer.itemId">
|
||||
<div v-if="!composer.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
|
||||
<div class="media-content is-clipped">
|
||||
<span
|
||||
:id="'index_' + composer.groupKey"
|
||||
class="tag is-info is-light is-small has-text-weight-bold"
|
||||
>{{ composer.groupKey }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<list-item-composer v-for="composer in composers_list"
|
||||
:key="composer.id"
|
||||
:composer="composer"
|
||||
@click="open_composer(composer)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(composer)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-composer>
|
||||
<div
|
||||
v-else-if="composer.isItem"
|
||||
class="media"
|
||||
@click="open_composer(composer.item)"
|
||||
>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">
|
||||
{{ composer.item.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(composer.item)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<modal-dialog-composer :show="show_details_modal" :composer="selected_composer" :media_kind="media_kind" @close="show_details_modal = false" />
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-composer
|
||||
:show="show_details_modal"
|
||||
:composer="selected_composer"
|
||||
:media_kind="media_kind"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemComposer from '@/components/ListItemComposer'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer'
|
||||
import Composers from '@/lib/Composers'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
|
||||
|
||||
export default {
|
||||
name: 'ListComposers',
|
||||
components: { ListItemComposer, ModalDialogComposer },
|
||||
components: { ModalDialogComposer },
|
||||
|
||||
props: ['composers', 'media_kind'],
|
||||
props: ['composers', 'media_kind', 'hide_group_title'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_composer: {}
|
||||
@ -51,25 +56,19 @@ export default {
|
||||
|
||||
computed: {
|
||||
media_kind_resolved: function () {
|
||||
return this.media_kind ? this.media_kind : this.selected_composer.media_kind
|
||||
},
|
||||
|
||||
composers_list: function () {
|
||||
if (Array.isArray(this.composers)) {
|
||||
return this.composers
|
||||
}
|
||||
return this.composers.sortedAndFiltered
|
||||
},
|
||||
|
||||
is_grouped: function () {
|
||||
return (this.composers instanceof Composers && this.composers.options.group)
|
||||
return this.media_kind
|
||||
? this.media_kind
|
||||
: this.selected_composer.media_kind
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_composer: function (composer) {
|
||||
this.selected_composer = composer
|
||||
this.$router.push({ name: 'ComposerTracks', params: { composer: composer.name } })
|
||||
this.$router.push({
|
||||
name: 'ComposerTracks',
|
||||
params: { composer: composer.name }
|
||||
})
|
||||
},
|
||||
|
||||
open_dialog: function (composer) {
|
||||
@ -80,5 +79,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
116
web-src/src/components/ListDirectories.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="$route.query.directory"
|
||||
class="media"
|
||||
@click="open_parent_directory()"
|
||||
>
|
||||
<figure class="media-left fd-has-action">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-subdirectory-arrow-left" />
|
||||
</span>
|
||||
</figure>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">..</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="directory in directories" :key="directory.path">
|
||||
<div class="media" @click="open_directory(directory)">
|
||||
<figure class="media-left fd-has-action">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-folder" />
|
||||
</span>
|
||||
</figure>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">
|
||||
{{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}
|
||||
</h1>
|
||||
<h2 class="subtitle is-7 has-text-grey-light">
|
||||
{{ directory.path }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(directory)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-directory
|
||||
:show="show_details_modal"
|
||||
:directory="selected_directory"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalDialogDirectory from '@/components/ModalDialogDirectory.vue'
|
||||
|
||||
export default {
|
||||
name: 'ListDirectories',
|
||||
components: { ModalDialogDirectory },
|
||||
|
||||
props: ['directories'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_directory: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
current_directory() {
|
||||
if (this.$route.query && this.$route.query.directory) {
|
||||
return this.$route.query.directory
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_parent_directory: function () {
|
||||
const parent = this.current_directory.slice(
|
||||
0,
|
||||
this.current_directory.lastIndexOf('/')
|
||||
)
|
||||
if (
|
||||
parent === '' ||
|
||||
this.$store.state.config.directories.includes(this.current_directory)
|
||||
) {
|
||||
this.$router.push({ path: '/files' })
|
||||
} else {
|
||||
this.$router.push({
|
||||
path: '/files',
|
||||
query: {
|
||||
directory: this.current_directory.slice(
|
||||
0,
|
||||
this.current_directory.lastIndexOf('/')
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
open_directory: function (directory) {
|
||||
this.$router.push({
|
||||
path: '/files',
|
||||
query: { directory: directory.path }
|
||||
})
|
||||
},
|
||||
|
||||
open_dialog: function (directory) {
|
||||
this.selected_directory = directory
|
||||
this.show_details_modal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
71
web-src/src/components/ListGenres.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<template v-for="genre in genres" :key="genre.itemId">
|
||||
<div v-if="!genre.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
|
||||
<div class="media-content is-clipped">
|
||||
<span
|
||||
:id="'index_' + genre.groupKey"
|
||||
class="tag is-info is-light is-small has-text-weight-bold"
|
||||
>{{ genre.groupKey }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="genre.isItem" class="media" @click="open_genre(genre.item)">
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">
|
||||
{{ genre.item.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(genre.item)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-genre
|
||||
:show="show_details_modal"
|
||||
:genre="selected_genre"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
|
||||
|
||||
export default {
|
||||
name: 'ListGenres',
|
||||
components: { ModalDialogGenre },
|
||||
|
||||
props: ['genres', 'media_kind', 'hide_group_title'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_genre: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
media_kind_resolved: function () {
|
||||
return this.media_kind ? this.media_kind : this.selected_genre.media_kind
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_genre: function (genre) {
|
||||
this.$router.push({ name: 'Genre', params: { genre: genre.name } })
|
||||
},
|
||||
|
||||
open_dialog: function (genre) {
|
||||
this.selected_genre = genre
|
||||
this.show_details_modal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -1,32 +0,0 @@
|
||||
<template functional>
|
||||
<div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()">
|
||||
<div class="media-left fd-has-action"
|
||||
v-if="$slots['artwork']"
|
||||
@click="listeners.click">
|
||||
<slot name="artwork"></slot>
|
||||
</div>
|
||||
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
||||
<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>
|
||||
<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') }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-right" style="padding-top:0.7rem;">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemAlbum',
|
||||
props: ['album', 'media_kind']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,20 +0,0 @@
|
||||
<template functional>
|
||||
<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>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemArtist',
|
||||
props: ['artist']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,20 +0,0 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemComposer',
|
||||
props: ['composer']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,26 +0,0 @@
|
||||
<template functional>
|
||||
<div class="media">
|
||||
<figure class="media-left fd-has-action" @click="listeners.click">
|
||||
<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>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemDirectory',
|
||||
props: ['directory']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,20 +0,0 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemGenre',
|
||||
props: ['genre']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,23 +0,0 @@
|
||||
<template functional>
|
||||
<div class="media">
|
||||
<figure class="media-left fd-has-action" v-if="slots().icon" @click="listeners.click">
|
||||
<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>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemPlaylist',
|
||||
props: ['playlist']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,16 +1,44 @@
|
||||
<template>
|
||||
<div class="media" v-if="is_next || !show_only_next_items">
|
||||
<div class="media-left" v-if="edit_mode">
|
||||
<span class="icon has-text-grey fd-is-movable handle"><i class="mdi mdi-drag-horizontal mdi-18px"></i></span>
|
||||
<div v-if="is_next || !show_only_next_items" class="media">
|
||||
<div v-if="edit_mode" class="media-left">
|
||||
<span class="icon has-text-grey fd-is-movable handle"
|
||||
><i class="mdi mdi-drag-horizontal mdi-18px"
|
||||
/></span>
|
||||
</div>
|
||||
|
||||
<div class="media-content fd-has-action is-clipped" v-on:click="play">
|
||||
<h1 class="title is-6" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next }">{{ item.title }}</h1>
|
||||
<h2 class="subtitle is-7" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next, 'has-text-grey': is_next && item.id !== state.item_id }"><b>{{ item.artist }}</b></h2>
|
||||
<h2 class="subtitle is-7" :class="{ 'has-text-primary': item.id === state.item_id, 'has-text-grey-light': !is_next, 'has-text-grey': is_next && item.id !== state.item_id }">{{ item.album }}</h2>
|
||||
<div class="media-content fd-has-action is-clipped" @click="play">
|
||||
<h1
|
||||
class="title is-6"
|
||||
:class="{
|
||||
'has-text-primary': item.id === state.item_id,
|
||||
'has-text-grey-light': !is_next
|
||||
}"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h1>
|
||||
<h2
|
||||
class="subtitle is-7"
|
||||
:class="{
|
||||
'has-text-primary': item.id === state.item_id,
|
||||
'has-text-grey-light': !is_next,
|
||||
'has-text-grey': is_next && item.id !== state.item_id
|
||||
}"
|
||||
>
|
||||
<b>{{ item.artist }}</b>
|
||||
</h2>
|
||||
<h2
|
||||
class="subtitle is-7"
|
||||
:class="{
|
||||
'has-text-primary': item.id === state.item_id,
|
||||
'has-text-grey-light': !is_next,
|
||||
'has-text-grey': is_next && item.id !== state.item_id
|
||||
}"
|
||||
>
|
||||
{{ item.album }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -20,14 +48,20 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ListItemQueueItem',
|
||||
props: ['item', 'position', 'current_position', 'show_only_next_items', 'edit_mode'],
|
||||
props: [
|
||||
'item',
|
||||
'position',
|
||||
'current_position',
|
||||
'show_only_next_items',
|
||||
'edit_mode'
|
||||
],
|
||||
|
||||
computed: {
|
||||
state () {
|
||||
state() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
|
||||
is_next () {
|
||||
is_next() {
|
||||
return this.current_position < 0 || this.position >= this.current_position
|
||||
}
|
||||
},
|
||||
@ -40,5 +74,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,26 +0,0 @@
|
||||
<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">
|
||||
<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>
|
||||
<slot name="progress"></slot>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListItemTrack',
|
||||
props: ['track']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,32 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
||||
<template slot="icon">
|
||||
<span class="icon">
|
||||
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(playlist)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-playlist>
|
||||
<modal-dialog-playlist :show="show_details_modal" :playlist="selected_playlist" @close="show_details_modal = false" />
|
||||
<div
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
class="media"
|
||||
:playlist="playlist"
|
||||
@click="open_playlist(playlist)"
|
||||
>
|
||||
<figure class="media-left fd-has-action">
|
||||
<span class="icon">
|
||||
<i
|
||||
class="mdi"
|
||||
:class="{
|
||||
'mdi-library-music': playlist.type !== 'folder',
|
||||
'mdi-rss': playlist.type === 'rss',
|
||||
'mdi-folder': playlist.type === 'folder'
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
</figure>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1 class="title is-6">
|
||||
{{ playlist.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(playlist)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="#app">
|
||||
<modal-dialog-playlist
|
||||
:show="show_details_modal"
|
||||
:playlist="selected_playlist"
|
||||
@close="show_details_modal = false"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
|
||||
|
||||
export default {
|
||||
name: 'ListPlaylists',
|
||||
components: { ListItemPlaylist, ModalDialogPlaylist },
|
||||
components: { ModalDialogPlaylist },
|
||||
|
||||
props: ['playlists'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_playlist: {}
|
||||
@ -50,5 +73,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,28 +1,71 @@
|
||||
<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)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||
<div
|
||||
v-for="(track, index) in tracks"
|
||||
:id="'index_' + track.title_sort.charAt(0).toUpperCase()"
|
||||
:key="track.id"
|
||||
class="media"
|
||||
:class="{ 'with-progress': show_progress }"
|
||||
@click="play_track(index, track)"
|
||||
>
|
||||
<figure v-if="show_icon" class="media-left fd-has-action">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-file-outline" />
|
||||
</span>
|
||||
</figure>
|
||||
<div class="media-content fd-has-action is-clipped">
|
||||
<h1
|
||||
class="title is-6"
|
||||
:class="{
|
||||
'has-text-grey':
|
||||
track.media_kind === 'podcast' && track.play_count > 0
|
||||
}"
|
||||
>
|
||||
{{ track.title }}
|
||||
</h1>
|
||||
<h2 class="subtitle is-7 has-text-grey">
|
||||
<b>{{ track.artist }}</b>
|
||||
</h2>
|
||||
<h2 class="subtitle is-7 has-text-grey">
|
||||
{{ track.album }}
|
||||
</h2>
|
||||
<progress-bar
|
||||
v-if="show_progress"
|
||||
:max="track.length_ms"
|
||||
:value="track.seek_ms"
|
||||
/>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click.prevent.stop="open_dialog(track)">
|
||||
<span class="icon has-text-dark"
|
||||
><i class="mdi mdi-dots-vertical mdi-18px"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<teleport to="#app">
|
||||
<modal-dialog-track
|
||||
:show="show_details_modal"
|
||||
:track="selected_track"
|
||||
@close="show_details_modal = false"
|
||||
@play-count-changed="$emit('play-count-changed')"
|
||||
/>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ListTracks',
|
||||
components: { ListItemTrack, ModalDialogTrack },
|
||||
components: { ModalDialogTrack, ProgressBar },
|
||||
|
||||
props: ['tracks', 'uris', 'expression'],
|
||||
props: ['tracks', 'uris', 'expression', 'show_progress', 'show_icon'],
|
||||
emits: ['play-count-changed'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_details_modal: false,
|
||||
selected_track: {}
|
||||
@ -48,5 +91,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,30 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4" v-if="title">
|
||||
<p v-if="title" class="title is-4">
|
||||
{{ title }}
|
||||
</p>
|
||||
<slot name="modal-content"></slot>
|
||||
<slot name="modal-content" />
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="$emit('close')">
|
||||
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">{{ close_action ? close_action : 'Cancel' }}</span>
|
||||
<span class="icon"><i class="mdi mdi-cancel" /></span>
|
||||
<span class="is-size-7">{{
|
||||
close_action ? close_action : 'Cancel'
|
||||
}}</span>
|
||||
</a>
|
||||
<a v-if="delete_action" class="card-footer-item has-background-danger has-text-white has-text-weight-bold" @click="$emit('delete')">
|
||||
<span class="icon"><i class="mdi mdi-delete"></i></span> <span class="is-size-7">{{ delete_action }}</span>
|
||||
<a
|
||||
v-if="delete_action"
|
||||
class="card-footer-item has-background-danger has-text-white has-text-weight-bold"
|
||||
@click="$emit('delete')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-delete" /></span>
|
||||
<span class="is-size-7">{{ delete_action }}</span>
|
||||
</a>
|
||||
<a v-if="ok_action" class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="$emit('ok')">
|
||||
<span class="icon"><i class="mdi mdi-check"></i></span> <span class="is-size-7">{{ ok_action }}</span>
|
||||
<a
|
||||
v-if="ok_action"
|
||||
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
|
||||
@click="$emit('ok')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-check" /></span>
|
||||
<span class="is-size-7">{{ ok_action }}</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -33,9 +50,9 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ModalDialog',
|
||||
props: ['show', 'title', 'ok_action', 'delete_action', 'close_action']
|
||||
props: ['show', 'title', 'ok_action', 'delete_action', 'close_action'],
|
||||
emits: ['delete', 'close', 'ok']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -10,32 +10,54 @@
|
||||
<form @submit.prevent="add_stream">
|
||||
<div class="field">
|
||||
<p class="control is-expanded has-icons-left">
|
||||
<input class="input is-shadowless" type="text" placeholder="http://url-to-rss" v-model="url" :disabled="loading" ref="url_field">
|
||||
<input
|
||||
ref="url_field"
|
||||
v-model="url"
|
||||
class="input is-shadowless"
|
||||
type="text"
|
||||
placeholder="http://url-to-rss"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<i class="mdi mdi-rss"></i>
|
||||
<i class="mdi mdi-rss" />
|
||||
</span>
|
||||
</p>
|
||||
<p class="help">Adding a podcast includes creating an RSS playlist, that will allow OwnTone to manage the podcast subscription.
|
||||
<p class="help">
|
||||
Adding a podcast includes creating an RSS playlist, that
|
||||
will allow OwnTone to manage the podcast subscription.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="loading">
|
||||
<footer v-if="loading" class="card-footer">
|
||||
<a class="card-footer-item button is-loading">
|
||||
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Processing ...</span>
|
||||
<span class="icon"><i class="mdi mdi-web" /></span>
|
||||
<span class="is-size-7">Processing ...</span>
|
||||
</a>
|
||||
</footer>
|
||||
<footer class="card-footer" v-else>
|
||||
<a class="card-footer-item has-text-danger" @click="$emit('close')">
|
||||
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
|
||||
<footer v-else class="card-footer">
|
||||
<a
|
||||
class="card-footer-item has-text-danger"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-cancel" /></span>
|
||||
<span class="is-size-7">Cancel</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="add_stream">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<a
|
||||
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
|
||||
@click="add_stream"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -47,29 +69,17 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogAddRss',
|
||||
props: ['show'],
|
||||
emits: ['close', 'podcast-added'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add_stream: function () {
|
||||
this.loading = true
|
||||
webapi.library_add(this.url).then(() => {
|
||||
this.$emit('close')
|
||||
this.$emit('podcast-added')
|
||||
this.url = ''
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show' () {
|
||||
show() {
|
||||
if (this.show) {
|
||||
this.loading = false
|
||||
|
||||
@ -79,9 +89,24 @@ export default {
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add_stream: function () {
|
||||
this.loading = true
|
||||
webapi
|
||||
.library_add(this.url)
|
||||
.then(() => {
|
||||
this.$emit('close')
|
||||
this.$emit('podcast-added')
|
||||
this.url = ''
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,44 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
Add stream URL
|
||||
</p>
|
||||
<form v-on:submit.prevent="play" class="fd-has-margin-bottom">
|
||||
<p class="title is-4">Add stream URL</p>
|
||||
<form class="fd-has-margin-bottom" @submit.prevent="play">
|
||||
<div class="field">
|
||||
<p class="control is-expanded has-icons-left">
|
||||
<input class="input is-shadowless" type="text" placeholder="http://url-to-stream" v-model="url" :disabled="loading" ref="url_field">
|
||||
<input
|
||||
ref="url_field"
|
||||
v-model="url"
|
||||
class="input is-shadowless"
|
||||
type="text"
|
||||
placeholder="http://url-to-stream"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<i class="mdi mdi-web"></i>
|
||||
<i class="mdi mdi-web" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="loading">
|
||||
<footer v-if="loading" class="card-footer">
|
||||
<a class="card-footer-item has-text-dark">
|
||||
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Loading ...</span>
|
||||
<span class="icon"><i class="mdi mdi-web" /></span>
|
||||
<span class="is-size-7">Loading ...</span>
|
||||
</a>
|
||||
</footer>
|
||||
<footer class="card-footer" v-else>
|
||||
<a class="card-footer-item has-text-danger" @click="$emit('close')">
|
||||
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
|
||||
<footer v-else class="card-footer">
|
||||
<a
|
||||
class="card-footer-item has-text-danger"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-cancel" /></span>
|
||||
<span class="is-size-7">Cancel</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="add_stream">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<a
|
||||
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
|
||||
@click="play"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -50,38 +69,17 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogAddUrlStream',
|
||||
props: ['show'],
|
||||
emits: ['close'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add_stream: function () {
|
||||
this.loading = true
|
||||
webapi.queue_add(this.url).then(() => {
|
||||
this.$emit('close')
|
||||
this.url = ''
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
play: function () {
|
||||
this.loading = true
|
||||
webapi.player_play_uri(this.url, false).then(() => {
|
||||
this.$emit('close')
|
||||
this.url = ''
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show' () {
|
||||
show() {
|
||||
if (this.show) {
|
||||
this.loading = false
|
||||
|
||||
@ -91,9 +89,36 @@ export default {
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add_stream: function () {
|
||||
this.loading = true
|
||||
webapi
|
||||
.queue_add(this.url)
|
||||
.then(() => {
|
||||
this.$emit('close')
|
||||
this.url = ''
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
play: function () {
|
||||
this.loading = true
|
||||
webapi
|
||||
.player_play_uri(this.url, false)
|
||||
.then(() => {
|
||||
this.$emit('close')
|
||||
this.url = ''
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -10,22 +10,33 @@
|
||||
:artwork_url="album.artwork_url"
|
||||
:artist="album.artist"
|
||||
:album="album.name"
|
||||
class="image is-square fd-has-margin-bottom fd-has-shadow" />
|
||||
class="image is-square fd-has-margin-bottom fd-has-shadow"
|
||||
/>
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
|
||||
<a class="has-text-link" @click="open_album">{{
|
||||
album.name
|
||||
}}</a>
|
||||
</p>
|
||||
<div class="buttons" v-if="media_kind_resolved === 'podcast'">
|
||||
<a class="button is-small" @click="mark_played">Mark as played</a>
|
||||
<a class="button is-small" @click="$emit('remove-podcast')">Remove podcast</a>
|
||||
<div v-if="media_kind_resolved === 'podcast'" class="buttons">
|
||||
<a class="button is-small" @click="mark_played"
|
||||
>Mark as played</a
|
||||
>
|
||||
<a class="button is-small" @click="$emit('remove-podcast')"
|
||||
>Remove podcast</a
|
||||
>
|
||||
</div>
|
||||
<div class="content is-small">
|
||||
<p v-if="album.artist">
|
||||
<span class="heading">Album artist</span>
|
||||
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
|
||||
<a class="title is-6 has-text-link" @click="open_artist">{{
|
||||
album.artist
|
||||
}}</a>
|
||||
</p>
|
||||
<p v-if="album.date_released">
|
||||
<span class="heading">Release date</span>
|
||||
<span class="title is-6">{{ 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,47 +48,61 @@
|
||||
</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>
|
||||
<span class="title is-6">{{ album.media_kind }} - {{ album.data_kind }}</span>
|
||||
<span class="title is-6"
|
||||
>{{ album.media_kind }} - {{ album.data_kind }}</span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Added at</span>
|
||||
<span class="title is-6">{{ album.time_added | time('L LT') }}</span>
|
||||
<span class="title is-6">{{
|
||||
$filters.time(album.time_added, 'L LT')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import CoverArtwork from '@/components/CoverArtwork.vue'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogAlbum',
|
||||
components: { CoverArtwork },
|
||||
props: ['show', 'album', 'media_kind', 'new_tracks'],
|
||||
emits: ['close', 'remove-podcast', 'play-count-changed'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
artwork_visible: false
|
||||
}
|
||||
@ -123,17 +148,21 @@ export default {
|
||||
if (this.media_kind_resolved === 'podcast') {
|
||||
// No artist page for podcasts
|
||||
} else if (this.media_kind_resolved === 'audiobook') {
|
||||
this.$router.push({ path: '/audiobooks/artists/' + this.album.artist_id })
|
||||
this.$router.push({
|
||||
path: '/audiobooks/artists/' + this.album.artist_id
|
||||
})
|
||||
} else {
|
||||
this.$router.push({ path: '/music/artists/' + this.album.artist_id })
|
||||
}
|
||||
},
|
||||
|
||||
mark_played: function () {
|
||||
webapi.library_album_track_update(this.album.id, { play_count: 'played' }).then(({ data }) => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
webapi
|
||||
.library_album_track_update(this.album.id, { play_count: 'played' })
|
||||
.then(({ data }) => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
},
|
||||
|
||||
artwork_loaded: function () {
|
||||
@ -147,5 +176,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_artist">{{ artist.name }}</a>
|
||||
<a class="has-text-link" @click="open_artist">{{
|
||||
artist.name
|
||||
}}</a>
|
||||
</p>
|
||||
<div class="content is-small">
|
||||
<p>
|
||||
@ -24,24 +26,33 @@
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Added at</span>
|
||||
<span class="title is-6">{{ artist.time_added | time('L LT') }}</span>
|
||||
<span class="title is-6">{{
|
||||
$filters.time(artist.time_added, 'L LT')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -53,6 +64,7 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogArtist',
|
||||
props: ['show', 'artist'],
|
||||
emits: ['close'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
@ -78,5 +90,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,37 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_albums">{{ composer.name }}</a>
|
||||
<a class="has-text-link" @click="open_albums">{{
|
||||
composer.name
|
||||
}}</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Albums</span>
|
||||
<a class="has-text-link is-6" @click="open_albums">{{ composer.album_count }}</a>
|
||||
<a class="has-text-link is-6" @click="open_albums">{{
|
||||
composer.album_count
|
||||
}}</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Tracks</span>
|
||||
<a class="has-text-link is-6" @click="open_tracks">{{ composer.track_count }}</a>
|
||||
<a class="has-text-link is-6" @click="open_tracks">{{
|
||||
composer.track_count
|
||||
}}</a>
|
||||
</p>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -43,35 +56,48 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogComposer',
|
||||
props: ['show', 'composer'],
|
||||
emits: ['close'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
this.$emit('close')
|
||||
webapi.player_play_expression('composer is "' + this.composer.name + '" and media_kind is music', false)
|
||||
webapi.player_play_expression(
|
||||
'composer is "' + this.composer.name + '" and media_kind is music',
|
||||
false
|
||||
)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add('composer is "' + this.composer.name + '" and media_kind is music')
|
||||
webapi.queue_expression_add(
|
||||
'composer is "' + this.composer.name + '" and media_kind is music'
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add_next('composer is "' + this.composer.name + '" and media_kind is music')
|
||||
webapi.queue_expression_add_next(
|
||||
'composer is "' + this.composer.name + '" and media_kind is music'
|
||||
)
|
||||
},
|
||||
|
||||
open_albums: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ name: 'ComposerAlbums', params: { composer: this.composer.name } })
|
||||
this.$router.push({
|
||||
name: 'ComposerAlbums',
|
||||
params: { composer: this.composer.name }
|
||||
})
|
||||
},
|
||||
|
||||
open_tracks: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ name: 'ComposerTracks', params: { composer: this.composer.name } })
|
||||
this.$router.push({
|
||||
name: 'ComposerTracks',
|
||||
params: { composer: this.composer.name }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -12,18 +12,25 @@
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -35,25 +42,32 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogDirectory',
|
||||
props: ['show', 'directory'],
|
||||
emits: ['close'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
this.$emit('close')
|
||||
webapi.player_play_expression('path starts with "' + this.directory.path + '" order by path asc', false)
|
||||
webapi.player_play_expression(
|
||||
'path starts with "' + this.directory.path + '" order by path asc',
|
||||
false
|
||||
)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add('path starts with "' + this.directory.path + '" order by path asc')
|
||||
webapi.queue_expression_add(
|
||||
'path starts with "' + this.directory.path + '" order by path asc'
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add_next('path starts with "' + this.directory.path + '" order by path asc')
|
||||
webapi.queue_expression_add_next(
|
||||
'path starts with "' + this.directory.path + '" order by path asc'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,29 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_genre">{{ genre.name }}</a>
|
||||
<a class="has-text-link" @click="open_genre">{{
|
||||
genre.name
|
||||
}}</a>
|
||||
</p>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -35,21 +44,29 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogGenre',
|
||||
props: ['show', 'genre'],
|
||||
emits: ['close'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
this.$emit('close')
|
||||
webapi.player_play_expression('genre is "' + this.genre.name + '" and media_kind is music', false)
|
||||
webapi.player_play_expression(
|
||||
'genre is "' + this.genre.name + '" and media_kind is music',
|
||||
false
|
||||
)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add('genre is "' + this.genre.name + '" and media_kind is music')
|
||||
webapi.queue_expression_add(
|
||||
'genre is "' + this.genre.name + '" and media_kind is music'
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.$emit('close')
|
||||
webapi.queue_expression_add_next('genre is "' + this.genre.name + '" and media_kind is music')
|
||||
webapi.queue_expression_add_next(
|
||||
'genre is "' + this.genre.name + '" and media_kind is music'
|
||||
)
|
||||
},
|
||||
|
||||
open_genre: function () {
|
||||
@ -60,5 +77,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_playlist">{{ playlist.name }}</a>
|
||||
<a class="has-text-link" @click="open_playlist">{{
|
||||
playlist.name
|
||||
}}</a>
|
||||
</p>
|
||||
<div class="content is-small">
|
||||
<p>
|
||||
@ -20,20 +22,27 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="!playlist.folder">
|
||||
<footer v-if="!playlist.folder" class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -45,6 +54,7 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogPlaylist',
|
||||
props: ['show', 'playlist', 'uris'],
|
||||
emits: ['close'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
@ -70,5 +80,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,41 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
Save queue to playlist
|
||||
</p>
|
||||
<form v-on:submit.prevent="save" class="fd-has-margin-bottom">
|
||||
<p class="title is-4">Save queue to playlist</p>
|
||||
<form class="fd-has-margin-bottom" @submit.prevent="save">
|
||||
<div class="field">
|
||||
<p class="control is-expanded has-icons-left">
|
||||
<input class="input is-shadowless" type="text" placeholder="Playlist name" v-model="playlist_name" :disabled="loading" ref="playlist_name_field">
|
||||
<input
|
||||
ref="playlist_name_field"
|
||||
v-model="playlist_name"
|
||||
class="input is-shadowless"
|
||||
type="text"
|
||||
placeholder="Playlist name"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<i class="mdi mdi-file-music"></i>
|
||||
<i class="mdi mdi-file-music" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="card-footer" v-if="loading">
|
||||
<footer v-if="loading" class="card-footer">
|
||||
<a class="card-footer-item has-text-dark">
|
||||
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Saving ...</span>
|
||||
<span class="icon"><i class="mdi mdi-web" /></span>
|
||||
<span class="is-size-7">Saving ...</span>
|
||||
</a>
|
||||
</footer>
|
||||
<footer class="card-footer" v-else>
|
||||
<a class="card-footer-item has-text-danger" @click="$emit('close')">
|
||||
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
|
||||
<footer v-else class="card-footer">
|
||||
<a
|
||||
class="card-footer-item has-text-danger"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-cancel" /></span>
|
||||
<span class="is-size-7">Cancel</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="save">
|
||||
<span class="icon"><i class="mdi mdi-content-save"></i></span> <span class="is-size-7">Save</span>
|
||||
<a
|
||||
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
|
||||
@click="save"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-content-save" /></span>
|
||||
<span class="is-size-7">Save</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -47,14 +65,28 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogPlaylistSave',
|
||||
props: ['show'],
|
||||
emits: ['close'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
playlist_name: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
show() {
|
||||
if (this.show) {
|
||||
this.loading = false
|
||||
|
||||
// We need to delay setting the focus to the input field until the field is part of the dom and visible
|
||||
setTimeout(() => {
|
||||
this.$refs.playlist_name_field.focus()
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save: function () {
|
||||
if (this.playlist_name.length < 1) {
|
||||
@ -62,29 +94,18 @@ export default {
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
webapi.queue_save_playlist(this.playlist_name).then(() => {
|
||||
this.$emit('close')
|
||||
this.playlist_name = ''
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show' () {
|
||||
if (this.show) {
|
||||
this.loading = false
|
||||
|
||||
// We need to delay setting the focus to the input field until the field is part of the dom and visible
|
||||
setTimeout(() => {
|
||||
this.$refs.playlist_name_field.focus()
|
||||
}, 10)
|
||||
}
|
||||
webapi
|
||||
.queue_save_playlist(this.playlist_name)
|
||||
.then(() => {
|
||||
this.$emit('close')
|
||||
this.playlist_name = ''
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -15,12 +15,22 @@
|
||||
<div class="content is-small">
|
||||
<p>
|
||||
<span class="heading">Album</span>
|
||||
<a v-if="item.album_id" class="title is-6 has-text-link" @click="open_album">{{ item.album }}</a>
|
||||
<a
|
||||
v-if="item.album_id"
|
||||
class="title is-6 has-text-link"
|
||||
@click="open_album"
|
||||
>{{ item.album }}</a
|
||||
>
|
||||
<span v-else class="title is-6">{{ item.album }}</span>
|
||||
</p>
|
||||
<p v-if="item.album_artist">
|
||||
<span class="heading">Album artist</span>
|
||||
<a v-if="item.album_artist_id" class="title is-6 has-text-link" @click="open_album_artist">{{ item.album_artist }}</a>
|
||||
<a
|
||||
v-if="item.album_artist_id"
|
||||
class="title is-6 has-text-link"
|
||||
@click="open_album_artist"
|
||||
>{{ item.album_artist }}</a
|
||||
>
|
||||
<span v-else class="title is-6">{{ item.album_artist }}</span>
|
||||
</p>
|
||||
<p v-if="item.composer">
|
||||
@ -33,15 +43,21 @@
|
||||
</p>
|
||||
<p v-if="item.genre">
|
||||
<span class="heading">Genre</span>
|
||||
<a class="title is-6 has-text-link" @click="open_genre">{{ item.genre }}</a>
|
||||
<a class="title is-6 has-text-link" @click="open_genre">{{
|
||||
item.genre
|
||||
}}</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Track / Disc</span>
|
||||
<span class="title is-6">{{ item.track_number }} / {{ item.disc_number }}</span>
|
||||
<span class="title is-6"
|
||||
>{{ item.track_number }} / {{ item.disc_number }}</span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Length</span>
|
||||
<span class="title is-6">{{ item.length_ms | duration }}</span>
|
||||
<span class="title is-6">{{
|
||||
$filters.duration(item.length_ms)
|
||||
}}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Path</span>
|
||||
@ -49,14 +65,26 @@
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Type</span>
|
||||
<span class="title is-6">{{ item.media_kind }} - {{ item.data_kind }} <span class="has-text-weight-normal" v-if="item.data_kind === 'spotify'">(<a @click="open_spotify_artist">artist</a>, <a @click="open_spotify_album">album</a>)</span></span>
|
||||
<span class="title is-6"
|
||||
>{{ item.media_kind }} - {{ item.data_kind }}
|
||||
<span
|
||||
v-if="item.data_kind === 'spotify'"
|
||||
class="has-text-weight-normal"
|
||||
>(<a @click="open_spotify_artist">artist</a>,
|
||||
<a @click="open_spotify_album">album</a>)</span
|
||||
></span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Quality</span>
|
||||
<span class="title is-6">
|
||||
{{ item.type }}
|
||||
<span v-if="item.samplerate"> | {{ item.samplerate }} Hz</span>
|
||||
<span v-if="item.channels"> | {{ item.channels | channels }}</span>
|
||||
<span v-if="item.samplerate">
|
||||
| {{ item.samplerate }} Hz</span
|
||||
>
|
||||
<span v-if="item.channels">
|
||||
| {{ $filters.channels(item.channels) }}</span
|
||||
>
|
||||
<span v-if="item.bitrate"> | {{ item.bitrate }} Kb/s</span>
|
||||
</span>
|
||||
</p>
|
||||
@ -64,15 +92,21 @@
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="remove">
|
||||
<span class="icon"><i class="mdi mdi-delete"></i></span> <span class="is-size-7">Remove</span>
|
||||
<span class="icon"><i class="mdi mdi-delete" /></span>
|
||||
<span class="is-size-7">Remove</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -85,13 +119,30 @@ import SpotifyWebApi from 'spotify-web-api-js'
|
||||
export default {
|
||||
name: 'ModalDialogQueueItem',
|
||||
props: ['show', 'item'],
|
||||
emits: ['close'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
spotify_track: {}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
item() {
|
||||
if (this.item && this.item.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi
|
||||
.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1))
|
||||
.then((response) => {
|
||||
this.spotify_track = response
|
||||
})
|
||||
} else {
|
||||
this.spotify_track = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove: function () {
|
||||
this.$emit('close')
|
||||
@ -123,30 +174,19 @@ export default {
|
||||
|
||||
open_spotify_artist: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/artists/' + this.spotify_track.artists[0].id })
|
||||
this.$router.push({
|
||||
path: '/music/spotify/artists/' + this.spotify_track.artists[0].id
|
||||
})
|
||||
},
|
||||
|
||||
open_spotify_album: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/albums/' + this.spotify_track.album.id })
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'item' () {
|
||||
if (this.item && this.item.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1)).then((response) => {
|
||||
this.spotify_track = response
|
||||
})
|
||||
} else {
|
||||
this.spotify_track = {}
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/music/spotify/albums/' + this.spotify_track.album.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,36 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
Remote pairing request
|
||||
</p>
|
||||
<form v-on:submit.prevent="kickoff_pairing">
|
||||
<p class="title is-4">Remote pairing request</p>
|
||||
<form @submit.prevent="kickoff_pairing">
|
||||
<label class="label">
|
||||
{{ pairing.remote }}
|
||||
</label>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter pairing code" v-model="pairing_req.pin" ref="pin_field">
|
||||
<input
|
||||
ref="pin_field"
|
||||
v-model="pairing_req.pin"
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Enter pairing code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-danger" @click="$emit('close')">
|
||||
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
|
||||
<a
|
||||
class="card-footer-item has-text-danger"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-cancel" /></span>
|
||||
<span class="is-size-7">Cancel</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="kickoff_pairing">
|
||||
<span class="icon"><i class="mdi mdi-cellphone-iphone"></i></span> <span class="is-size-7">Pair Remote</span>
|
||||
<a
|
||||
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
|
||||
@click="kickoff_pairing"
|
||||
>
|
||||
<span class="icon"><i class="mdi mdi-cellphone-iphone" /></span>
|
||||
<span class="is-size-7">Pair Remote</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -42,29 +58,22 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'ModalDialogRemotePairing',
|
||||
props: ['show'],
|
||||
emits: ['close'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
pairing_req: { pin: '' }
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pairing () {
|
||||
pairing() {
|
||||
return this.$store.state.pairing
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
kickoff_pairing () {
|
||||
webapi.pairing_kickoff(this.pairing_req).then(() => {
|
||||
this.pairing_req.pin = ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'show' () {
|
||||
show() {
|
||||
if (this.show) {
|
||||
this.loading = false
|
||||
|
||||
@ -74,9 +83,16 @@ export default {
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
kickoff_pairing() {
|
||||
webapi.pairing_kickoff(this.pairing_req).then(() => {
|
||||
this.pairing_req.pin = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div class="modal is-active" v-if="show">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div v-if="show" class="modal is-active">
|
||||
<div class="modal-background" @click="$emit('close')" />
|
||||
<div class="modal-content fd-modal-card">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -12,18 +12,34 @@
|
||||
<p class="subtitle">
|
||||
{{ track.artist }}
|
||||
</p>
|
||||
<div class="buttons" v-if="track.media_kind === 'podcast'">
|
||||
<a class="button is-small" v-if="track.play_count > 0" @click="mark_new">Mark as new</a>
|
||||
<a class="button is-small" v-if="track.play_count === 0" @click="mark_played">Mark as played</a>
|
||||
<div v-if="track.media_kind === 'podcast'" class="buttons">
|
||||
<a
|
||||
v-if="track.play_count > 0"
|
||||
class="button is-small"
|
||||
@click="mark_new"
|
||||
>Mark as new</a
|
||||
>
|
||||
<a
|
||||
v-if="track.play_count === 0"
|
||||
class="button is-small"
|
||||
@click="mark_played"
|
||||
>Mark as played</a
|
||||
>
|
||||
</div>
|
||||
<div class="content is-small">
|
||||
<p>
|
||||
<span class="heading">Album</span>
|
||||
<a class="title is-6 has-text-link" @click="open_album">{{ track.album }}</a>
|
||||
<a class="title is-6 has-text-link" @click="open_album">{{
|
||||
track.album
|
||||
}}</a>
|
||||
</p>
|
||||
<p v-if="track.album_artist && track.media_kind !== 'audiobook'">
|
||||
<p
|
||||
v-if="track.album_artist && track.media_kind !== 'audiobook'"
|
||||
>
|
||||
<span class="heading">Album artist</span>
|
||||
<a class="title is-6 has-text-link" @click="open_artist">{{ track.album_artist }}</a>
|
||||
<a class="title is-6 has-text-link" @click="open_artist">{{
|
||||
track.album_artist
|
||||
}}</a>
|
||||
</p>
|
||||
<p v-if="track.composer">
|
||||
<span class="heading">Composer</span>
|
||||
@ -31,7 +47,9 @@
|
||||
</p>
|
||||
<p v-if="track.date_released">
|
||||
<span class="heading">Release date</span>
|
||||
<span class="title is-6">{{ 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>
|
||||
@ -39,15 +57,21 @@
|
||||
</p>
|
||||
<p v-if="track.genre">
|
||||
<span class="heading">Genre</span>
|
||||
<a class="title is-6 has-text-link" @click="open_genre">{{ track.genre }}</a>
|
||||
<a class="title is-6 has-text-link" @click="open_genre">{{
|
||||
track.genre
|
||||
}}</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Track / Disc</span>
|
||||
<span class="title is-6">{{ track.track_number }} / {{ track.disc_number }}</span>
|
||||
<span class="title is-6"
|
||||
>{{ track.track_number }} / {{ track.disc_number }}</span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Length</span>
|
||||
<span class="title is-6">{{ track.length_ms | duration }}</span>
|
||||
<span class="title is-6">{{
|
||||
$filters.duration(track.length_ms)
|
||||
}}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Path</span>
|
||||
@ -55,24 +79,42 @@
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Type</span>
|
||||
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }} <span class="has-text-weight-normal" v-if="track.data_kind === 'spotify'">(<a @click="open_spotify_artist">artist</a>, <a @click="open_spotify_album">album</a>)</span></span>
|
||||
<span class="title is-6"
|
||||
>{{ track.media_kind }} - {{ track.data_kind }}
|
||||
<span
|
||||
v-if="track.data_kind === 'spotify'"
|
||||
class="has-text-weight-normal"
|
||||
>(<a @click="open_spotify_artist">artist</a>,
|
||||
<a @click="open_spotify_album">album</a>)</span
|
||||
></span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Quality</span>
|
||||
<span class="title is-6">
|
||||
{{ track.type }}
|
||||
<span v-if="track.samplerate"> | {{ track.samplerate }} Hz</span>
|
||||
<span v-if="track.channels"> | {{ track.channels | channels }}</span>
|
||||
<span v-if="track.bitrate"> | {{ track.bitrate }} Kb/s</span>
|
||||
<span v-if="track.samplerate">
|
||||
| {{ track.samplerate }} Hz</span
|
||||
>
|
||||
<span v-if="track.channels">
|
||||
| {{ $filters.channels(track.channels) }}</span
|
||||
>
|
||||
<span v-if="track.bitrate">
|
||||
| {{ track.bitrate }} Kb/s</span
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Added at</span>
|
||||
<span class="title is-6">{{ 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>
|
||||
<span class="title is-6">{{ Math.floor(track.rating / 10) }} / 10</span>
|
||||
<span class="title is-6"
|
||||
>{{ Math.floor(track.rating / 10) }} / 10</span
|
||||
>
|
||||
</p>
|
||||
<p v-if="track.comment">
|
||||
<span class="heading">Comment</span>
|
||||
@ -82,18 +124,25 @@
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus" /></span>
|
||||
<span class="is-size-7">Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
|
||||
<span class="icon"><i class="mdi mdi-playlist-play" /></span>
|
||||
<span class="is-size-7">Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play_track">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
|
||||
<span class="icon"><i class="mdi mdi-play" /></span>
|
||||
<span class="is-size-7">Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||
<button
|
||||
class="modal-close is-large"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -107,13 +156,30 @@ export default {
|
||||
name: 'ModalDialogTrack',
|
||||
|
||||
props: ['show', 'track'],
|
||||
emits: ['close', 'play-count-changed'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
spotify_track: {}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
track() {
|
||||
if (this.track && this.track.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi
|
||||
.getTrack(this.track.path.slice(this.track.path.lastIndexOf(':') + 1))
|
||||
.then((response) => {
|
||||
this.spotify_track = response
|
||||
})
|
||||
} else {
|
||||
this.spotify_track = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
play_track: function () {
|
||||
this.$emit('close')
|
||||
@ -143,7 +209,9 @@ export default {
|
||||
|
||||
open_artist: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/artists/' + this.track.album_artist_id })
|
||||
this.$router.push({
|
||||
path: '/music/artists/' + this.track.album_artist_id
|
||||
})
|
||||
},
|
||||
|
||||
open_genre: function () {
|
||||
@ -152,44 +220,37 @@ export default {
|
||||
|
||||
open_spotify_artist: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/artists/' + this.spotify_track.artists[0].id })
|
||||
this.$router.push({
|
||||
path: '/music/spotify/artists/' + this.spotify_track.artists[0].id
|
||||
})
|
||||
},
|
||||
|
||||
open_spotify_album: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/albums/' + this.spotify_track.album.id })
|
||||
this.$router.push({
|
||||
path: '/music/spotify/albums/' + this.spotify_track.album.id
|
||||
})
|
||||
},
|
||||
|
||||
mark_new: function () {
|
||||
webapi.library_track_update(this.track.id, { play_count: 'reset' }).then(() => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
webapi
|
||||
.library_track_update(this.track.id, { play_count: 'reset' })
|
||||
.then(() => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
},
|
||||
|
||||
mark_played: function () {
|
||||
webapi.library_track_update(this.track.id, { play_count: 'increment' }).then(() => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'track' () {
|
||||
if (this.track && this.track.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi.getTrack(this.track.path.slice(this.track.path.lastIndexOf(':') + 1)).then((response) => {
|
||||
this.spotify_track = response
|
||||
webapi
|
||||
.library_track_update(this.track.id, { play_count: 'increment' })
|
||||
.then(() => {
|
||||
this.$emit('play-count-changed')
|
||||
this.$emit('close')
|
||||
})
|
||||
} else {
|
||||
this.spotify_track = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,29 +1,34 @@
|
||||
<template>
|
||||
<modal-dialog
|
||||
:show="show"
|
||||
title="Update library"
|
||||
:ok_action="library.updating ? '' : 'Rescan'"
|
||||
close_action="Close"
|
||||
@ok="update_library"
|
||||
@close="close()">
|
||||
<template slot="modal-content">
|
||||
:show="show"
|
||||
title="Update library"
|
||||
:ok_action="library.updating ? '' : 'Rescan'"
|
||||
close_action="Close"
|
||||
@ok="update_library"
|
||||
@close="close()"
|
||||
>
|
||||
<template #modal-content>
|
||||
<div v-if="!library.updating">
|
||||
<p class="mb-3">Scan for new, deleted and modified files</p>
|
||||
<div class="field" v-if="spotify_enabled || rss.tracks > 0">
|
||||
<div v-if="spotify_enabled || rss.tracks > 0" class="field">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select v-model="update_dialog_scan_kind">
|
||||
<option value="">Update everything</option>
|
||||
<option value="files">Only update local library</option>
|
||||
<option value="spotify" v-if="spotify_enabled">Only update Spotify</option>
|
||||
<option value="rss" v-if="rss.tracks > 0">Only update RSS feeds</option>
|
||||
<option v-if="spotify_enabled" value="spotify">
|
||||
Only update Spotify
|
||||
</option>
|
||||
<option v-if="rss.tracks > 0" value="rss">
|
||||
Only update RSS feeds
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox is-size-7 is-small">
|
||||
<input type="checkbox" v-model="rescan_metadata">
|
||||
<input v-model="rescan_metadata" type="checkbox" />
|
||||
Rescan metadata for unmodified files
|
||||
</label>
|
||||
</div>
|
||||
@ -36,7 +41,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'
|
||||
|
||||
@ -44,38 +49,39 @@ export default {
|
||||
name: 'ModalDialogUpdate',
|
||||
components: { ModalDialog },
|
||||
props: ['show'],
|
||||
emits: ['close'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
rescan_metadata: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
library () {
|
||||
library() {
|
||||
return this.$store.state.library
|
||||
},
|
||||
|
||||
rss () {
|
||||
rss() {
|
||||
return this.$store.state.rss_count
|
||||
},
|
||||
|
||||
spotify_enabled () {
|
||||
spotify_enabled() {
|
||||
return this.$store.state.spotify.webapi_token_valid
|
||||
},
|
||||
|
||||
update_dialog_scan_kind: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.update_dialog_scan_kind
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.UPDATE_DIALOG_SCAN_KIND, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update_library () {
|
||||
update_library() {
|
||||
if (this.rescan_metadata) {
|
||||
webapi.library_rescan(this.update_dialog_scan_kind)
|
||||
} else {
|
||||
@ -83,7 +89,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
close () {
|
||||
close() {
|
||||
this.update_dialog_scan_kind = ''
|
||||
this.$emit('close')
|
||||
}
|
||||
@ -91,5 +97,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,66 +1,148 @@
|
||||
<template>
|
||||
<nav class="fd-bottom-navbar navbar is-white is-fixed-bottom" :style="zindex" :class="{ 'is-transparent': is_now_playing_page, 'is-dark': !is_now_playing_page }" role="navigation" aria-label="player controls">
|
||||
<nav
|
||||
class="fd-bottom-navbar navbar is-white is-fixed-bottom"
|
||||
:style="zindex"
|
||||
:class="{
|
||||
'is-transparent': is_now_playing_page,
|
||||
'is-dark': !is_now_playing_page
|
||||
}"
|
||||
role="navigation"
|
||||
aria-label="player controls"
|
||||
>
|
||||
<div class="navbar-brand fd-expanded">
|
||||
|
||||
<!-- Link to queue -->
|
||||
<navbar-item-link to="/" exact>
|
||||
<span class="icon"><i class="mdi mdi-24px mdi-playlist-play"></i></span>
|
||||
<span class="icon"><i class="mdi mdi-24px mdi-playlist-play" /></span>
|
||||
</navbar-item-link>
|
||||
|
||||
<!-- Now playing artist/title (not visible on "now playing" page) -->
|
||||
<router-link to="/now-playing" v-if="!is_now_playing_page" class="navbar-item is-expanded is-clipped" active-class="is-active" exact>
|
||||
<router-link
|
||||
v-if="!is_now_playing_page"
|
||||
to="/now-playing"
|
||||
class="navbar-item is-expanded is-clipped"
|
||||
active-class="is-active"
|
||||
exact
|
||||
>
|
||||
<div class="is-clipped">
|
||||
<p class="is-size-7 fd-is-text-clipped">
|
||||
<strong>{{ now_playing.title }}</strong><br>
|
||||
{{ now_playing.artist }}<span v-if="now_playing.data_kind === 'url'"> - {{ now_playing.album }}</span>
|
||||
<strong>{{ now_playing.title }}</strong
|
||||
><br />
|
||||
{{ now_playing.artist
|
||||
}}<span v-if="now_playing.data_kind === 'url'">
|
||||
- {{ now_playing.album }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Skip previous (not visible on "now playing" page) -->
|
||||
<player-button-previous v-if="is_now_playing_page" class="navbar-item fd-margin-left-auto" icon_style="mdi-24px"></player-button-previous>
|
||||
<player-button-seek-back v-if="is_now_playing_page" seek_ms="10000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-back>
|
||||
<player-button-previous
|
||||
v-if="is_now_playing_page"
|
||||
class="navbar-item fd-margin-left-auto"
|
||||
icon_style="mdi-24px"
|
||||
/>
|
||||
<player-button-seek-back
|
||||
v-if="is_now_playing_page"
|
||||
seek_ms="10000"
|
||||
class="navbar-item"
|
||||
icon_style="mdi-24px"
|
||||
/>
|
||||
<!-- Play/pause -->
|
||||
<player-button-play-pause class="navbar-item" icon_style="mdi-36px" show_disabled_message></player-button-play-pause>
|
||||
<player-button-seek-forward v-if="is_now_playing_page" seek_ms="30000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-forward>
|
||||
<player-button-play-pause
|
||||
class="navbar-item"
|
||||
icon_style="mdi-36px"
|
||||
show_disabled_message
|
||||
/>
|
||||
<player-button-seek-forward
|
||||
v-if="is_now_playing_page"
|
||||
seek_ms="30000"
|
||||
class="navbar-item"
|
||||
icon_style="mdi-24px"
|
||||
/>
|
||||
<!-- Skip next (not visible on "now playing" page) -->
|
||||
<player-button-next v-if="is_now_playing_page" class="navbar-item" icon_style="mdi-24px"></player-button-next>
|
||||
<player-button-next
|
||||
v-if="is_now_playing_page"
|
||||
class="navbar-item"
|
||||
icon_style="mdi-24px"
|
||||
/>
|
||||
|
||||
<!-- Player menu button (only visible on mobile and tablet) -->
|
||||
<a class="navbar-item fd-margin-left-auto is-hidden-desktop" @click="show_player_menu = !show_player_menu">
|
||||
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-chevron-up': !show_player_menu, 'mdi-chevron-down': show_player_menu }"></i></span>
|
||||
<a
|
||||
class="navbar-item fd-margin-left-auto is-hidden-desktop"
|
||||
@click="show_player_menu = !show_player_menu"
|
||||
>
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi mdi-18px"
|
||||
:class="{
|
||||
'mdi-chevron-up': !show_player_menu,
|
||||
'mdi-chevron-down': show_player_menu
|
||||
}"
|
||||
/></span>
|
||||
</a>
|
||||
|
||||
<!-- Player menu dropup menu (only visible on desktop) -->
|
||||
<div class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch"
|
||||
:class="{ 'is-active': show_player_menu }">
|
||||
<a class="navbar-link is-arrowless"
|
||||
@click="show_player_menu = !show_player_menu">
|
||||
<span class="icon"><i class="mdi mdi-18px"
|
||||
:class="{ 'mdi-chevron-up': !show_player_menu, 'mdi-chevron-down': show_player_menu }"></i></span>
|
||||
<div
|
||||
class="navbar-item has-dropdown has-dropdown-up fd-margin-left-auto is-hidden-touch"
|
||||
:class="{ 'is-active': show_player_menu }"
|
||||
>
|
||||
<a
|
||||
class="navbar-link is-arrowless"
|
||||
@click="show_player_menu = !show_player_menu"
|
||||
>
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi mdi-18px"
|
||||
:class="{
|
||||
'mdi-chevron-up': !show_player_menu,
|
||||
'mdi-chevron-down': show_player_menu
|
||||
}"
|
||||
/></span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-right is-boxed" style="margin-right: 6px; margin-bottom: 6px; border-radius: 6px;">
|
||||
<div
|
||||
class="navbar-dropdown is-right is-boxed"
|
||||
style="margin-right: 6px; margin-bottom: 6px; border-radius: 6px"
|
||||
>
|
||||
<div class="navbar-item">
|
||||
<!-- Outputs: master volume -->
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<a class="button is-white is-small" @click="toggle_mute_volume">
|
||||
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-volume-off': player.volume <= 0, 'mdi-volume-high': player.volume > 0 }"></i></span>
|
||||
<div class="level-item" style="flex-grow: 0">
|
||||
<a
|
||||
class="button is-white is-small"
|
||||
@click="toggle_mute_volume"
|
||||
>
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi mdi-18px"
|
||||
:class="{
|
||||
'mdi-volume-off': player.volume <= 0,
|
||||
'mdi-volume-high': player.volume > 0
|
||||
}"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading">Volume</p>
|
||||
<range-slider
|
||||
<Slider
|
||||
v-model="player.volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:tooltips="false"
|
||||
:classes="{ target: 'slider' }"
|
||||
@change="set_volume"
|
||||
/>
|
||||
<!--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>
|
||||
@ -68,21 +150,54 @@
|
||||
</div>
|
||||
|
||||
<!-- Outputs: master volume -->
|
||||
<hr class="fd-navbar-divider">
|
||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||
<hr class="fd-navbar-divider" />
|
||||
<navbar-item-output
|
||||
v-for="output in outputs"
|
||||
:key="output.id"
|
||||
:output="output"
|
||||
/>
|
||||
|
||||
<!-- Outputs: stream volume -->
|
||||
<hr class="fd-navbar-divider">
|
||||
<hr class="fd-navbar-divider" />
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a>
|
||||
<div class="level-item" style="flex-grow: 0">
|
||||
<a
|
||||
class="button is-white is-small"
|
||||
:class="{ 'is-loading': loading }"
|
||||
><span
|
||||
class="icon fd-has-action"
|
||||
:class="{
|
||||
'has-text-grey-light': !playing && !loading,
|
||||
'is-loading': loading
|
||||
}"
|
||||
@click="togglePlay"
|
||||
><i class="mdi mdi-18px mdi-radio-tower" /></span
|
||||
></a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
||||
<range-slider
|
||||
<p
|
||||
class="heading"
|
||||
:class="{ 'has-text-grey-light': !playing }"
|
||||
>
|
||||
HTTP stream
|
||||
<a href="stream.mp3"
|
||||
><span class="is-lowercase">(stream.mp3)</span></a
|
||||
>
|
||||
</p>
|
||||
<Slider
|
||||
v-model="stream_volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:tooltips="false"
|
||||
:disabled="!playing"
|
||||
:classes="{ target: 'slider' }"
|
||||
@change="set_stream_volume"
|
||||
/>
|
||||
<!--range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
@ -90,7 +205,7 @@
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</range-slider-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -98,14 +213,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Playback controls -->
|
||||
<hr class="fd-navbar-divider">
|
||||
<hr class="fd-navbar-divider" />
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile fd-expanded">
|
||||
<div class="level-item">
|
||||
<div class="buttons has-addons">
|
||||
<player-button-repeat class="button"></player-button-repeat>
|
||||
<player-button-shuffle class="button"></player-button-shuffle>
|
||||
<player-button-consume class="button"></player-button-consume>
|
||||
<player-button-repeat class="button" />
|
||||
<player-button-shuffle class="button" />
|
||||
<player-button-consume class="button" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,41 +230,59 @@
|
||||
</div>
|
||||
|
||||
<!-- Player menu (only visible on mobile and tablet) -->
|
||||
<div class="navbar-menu is-hidden-desktop" :class="{ 'is-active': show_player_menu }">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
<div
|
||||
class="navbar-menu is-hidden-desktop"
|
||||
:class="{ 'is-active': show_player_menu }"
|
||||
>
|
||||
<div class="navbar-start" />
|
||||
<div class="navbar-end">
|
||||
<!-- Repeat/shuffle/consume -->
|
||||
<div class="navbar-item">
|
||||
<div class="buttons is-centered">
|
||||
<player-button-repeat class="button" icon_style="mdi-18px"></player-button-repeat>
|
||||
<player-button-shuffle class="button" icon_style="mdi-18px"></player-button-shuffle>
|
||||
<player-button-consume class="button" icon_style="mdi-18px"></player-button-consume>
|
||||
<player-button-repeat class="button" icon_style="mdi-18px" />
|
||||
<player-button-shuffle class="button" icon_style="mdi-18px" />
|
||||
<player-button-consume class="button" icon_style="mdi-18px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="fd-navbar-divider">
|
||||
<hr class="fd-navbar-divider" />
|
||||
|
||||
<!-- Outputs: master volume -->
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<div class="level-item" style="flex-grow: 0">
|
||||
<a class="button is-white is-small" @click="toggle_mute_volume">
|
||||
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-volume-off': player.volume <= 0, 'mdi-volume-high': player.volume > 0 }"></i></span>
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi mdi-18px"
|
||||
:class="{
|
||||
'mdi-volume-off': player.volume <= 0,
|
||||
'mdi-volume-high': player.volume > 0
|
||||
}"
|
||||
/></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading">Volume</p>
|
||||
<range-slider
|
||||
<Slider
|
||||
v-model="player.volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:tooltips="false"
|
||||
:classes="{ target: 'slider' }"
|
||||
@change="set_volume"
|
||||
/>
|
||||
<!--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>
|
||||
@ -157,25 +290,55 @@
|
||||
</div>
|
||||
|
||||
<!-- Outputs: speaker volumes -->
|
||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||
<navbar-item-output
|
||||
v-for="output in outputs"
|
||||
:key="output.id"
|
||||
:output="output"
|
||||
/>
|
||||
|
||||
<!-- Outputs: stream volume -->
|
||||
<hr class="fd-navbar-divider">
|
||||
<hr class="fd-navbar-divider" />
|
||||
<div class="navbar-item fd-has-margin-bottom">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<a class="button is-white is-small" :class="{ 'is-loading': loading }">
|
||||
<span class="icon fd-has-action"
|
||||
:class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }"
|
||||
@click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i>
|
||||
<div class="level-item" style="flex-grow: 0">
|
||||
<a
|
||||
class="button is-white is-small"
|
||||
:class="{ 'is-loading': loading }"
|
||||
>
|
||||
<span
|
||||
class="icon fd-has-action"
|
||||
:class="{
|
||||
'has-text-grey-light': !playing && !loading,
|
||||
'is-loading': loading
|
||||
}"
|
||||
@click="togglePlay"
|
||||
><i class="mdi mdi-18px mdi-radio-tower" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
||||
<range-slider
|
||||
<p
|
||||
class="heading"
|
||||
:class="{ 'has-text-grey-light': !playing }"
|
||||
>
|
||||
HTTP stream
|
||||
<a href="stream.mp3"
|
||||
><span class="is-lowercase">(stream.mp3)</span></a
|
||||
>
|
||||
</p>
|
||||
<Slider
|
||||
v-model="stream_volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:tooltips="false"
|
||||
:disabled="!playing"
|
||||
:classes="{ target: 'slider' }"
|
||||
@change="set_stream_volume"
|
||||
/>
|
||||
<!-- range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
@ -183,7 +346,7 @@
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</range-slider-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -197,17 +360,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 +379,8 @@ export default {
|
||||
components: {
|
||||
NavbarItemLink,
|
||||
NavbarItemOutput,
|
||||
RangeSlider,
|
||||
//RangeSlider,
|
||||
Slider,
|
||||
PlayerButtonPlayPause,
|
||||
PlayerButtonNext,
|
||||
PlayerButtonPrevious,
|
||||
@ -226,7 +391,7 @@ export default {
|
||||
PlayerButtonSeekBack
|
||||
},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
old_volume: 0,
|
||||
|
||||
@ -241,49 +406,67 @@ export default {
|
||||
|
||||
computed: {
|
||||
show_player_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_burger_menu () {
|
||||
show_burger_menu() {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
|
||||
zindex () {
|
||||
zindex() {
|
||||
if (this.show_burger_menu) {
|
||||
return 'z-index: 20'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
state () {
|
||||
state() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
now_playing () {
|
||||
now_playing() {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
is_now_playing_page () {
|
||||
is_now_playing_page() {
|
||||
return this.$route.path === '/now-playing'
|
||||
},
|
||||
outputs () {
|
||||
outputs() {
|
||||
return this.$store.state.outputs
|
||||
},
|
||||
|
||||
player () {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
|
||||
config () {
|
||||
config() {
|
||||
return this.$store.state.config
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.player.volume'() {
|
||||
if (this.player.volume > 0) {
|
||||
this.old_volume = this.player.volume
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// on app mounted
|
||||
mounted() {
|
||||
this.setupAudio()
|
||||
},
|
||||
|
||||
// on app destroyed
|
||||
unmounted() {
|
||||
this.closeAudio()
|
||||
},
|
||||
|
||||
methods: {
|
||||
on_click_outside_outputs () {
|
||||
on_click_outside_outputs() {
|
||||
this.show_outputs_menu = false
|
||||
},
|
||||
|
||||
@ -302,21 +485,24 @@ export default {
|
||||
setupAudio: function () {
|
||||
const a = _audio.setupAudio()
|
||||
|
||||
a.addEventListener('waiting', e => {
|
||||
a.addEventListener('waiting', (e) => {
|
||||
this.playing = false
|
||||
this.loading = true
|
||||
})
|
||||
a.addEventListener('playing', e => {
|
||||
a.addEventListener('playing', (e) => {
|
||||
this.playing = true
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('ended', e => {
|
||||
a.addEventListener('ended', (e) => {
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('error', e => {
|
||||
a.addEventListener('error', (e) => {
|
||||
this.closeAudio()
|
||||
this.$store.dispatch('add_notification', { text: 'HTTP stream error: failed to load stream or stopped loading due to network problem', type: 'danger' })
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'HTTP stream error: failed to load stream or stopped loading due to network problem',
|
||||
type: 'danger'
|
||||
})
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
@ -353,27 +539,8 @@ export default {
|
||||
this.stream_volume = newVolume
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.player.volume' () {
|
||||
if (this.player.volume > 0) {
|
||||
this.old_volume = this.player.volume
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// on app mounted
|
||||
mounted () {
|
||||
this.setupAudio()
|
||||
},
|
||||
|
||||
// on app destroyed
|
||||
destroyed () {
|
||||
this.closeAudio()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<a class="navbar-item" :class="{ 'is-active': is_active }" @click.stop.prevent="open_link()" :href="full_path()">
|
||||
<slot></slot>
|
||||
<a
|
||||
class="navbar-item"
|
||||
:class="{ 'is-active': is_active }"
|
||||
:href="full_path()"
|
||||
@click.stop.prevent="open_link()"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,7 +20,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_active () {
|
||||
is_active() {
|
||||
if (this.exact) {
|
||||
return this.$route.path === this.to
|
||||
}
|
||||
@ -23,19 +28,19 @@ export default {
|
||||
},
|
||||
|
||||
show_player_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_PLAYER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,40 @@
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<div class="level-item" style="flex-grow: 0">
|
||||
<a class="button is-white is-small">
|
||||
<span class="icon fd-has-action"
|
||||
:class="{ 'has-text-grey-light': !output.selected }"
|
||||
v-on:click="set_enabled">
|
||||
<i class="mdi mdi-18px" :class="type_class" :title="output.type"></i>
|
||||
<span
|
||||
class="icon fd-has-action"
|
||||
:class="{ 'has-text-grey-light': !output.selected }"
|
||||
@click="set_enabled"
|
||||
>
|
||||
<i
|
||||
class="mdi mdi-18px"
|
||||
:class="type_class"
|
||||
:title="output.type"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading" :class="{ 'has-text-grey-light': !output.selected }">{{ output.name }}</p>
|
||||
<range-slider
|
||||
<p
|
||||
class="heading"
|
||||
:class="{ 'has-text-grey-light': !output.selected }"
|
||||
>
|
||||
{{ output.name }}
|
||||
</p>
|
||||
<Slider
|
||||
v-model="volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:tooltips="false"
|
||||
:disabled="!output.selected"
|
||||
:classes="{ target: 'slider' }"
|
||||
@change="set_volume"
|
||||
/>
|
||||
<!--range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
@ -22,7 +43,7 @@
|
||||
:disabled="!output.selected"
|
||||
:value="volume"
|
||||
@change="set_volume" >
|
||||
</range-slider>
|
||||
</range-slider-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,17 +52,21 @@
|
||||
</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'],
|
||||
|
||||
computed: {
|
||||
type_class () {
|
||||
type_class() {
|
||||
if (this.output.type.startsWith('AirPlay')) {
|
||||
return 'mdi-airplay'
|
||||
} else if (this.output.type === 'Chromecast') {
|
||||
@ -53,7 +78,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
volume () {
|
||||
volume() {
|
||||
return this.output.selected ? this.output.volume : 0
|
||||
}
|
||||
},
|
||||
@ -77,5 +102,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,167 +1,235 @@
|
||||
<template>
|
||||
<nav class="fd-top-navbar navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation">
|
||||
<nav
|
||||
class="fd-top-navbar navbar is-light is-fixed-top"
|
||||
:style="zindex"
|
||||
role="navigation"
|
||||
aria-label="main navigation"
|
||||
>
|
||||
<div class="navbar-brand">
|
||||
<navbar-item-link to="/playlists" v-if="is_visible_playlists">
|
||||
<span class="icon"><i class="mdi mdi-library-music"></i></span>
|
||||
<navbar-item-link v-if="is_visible_playlists" to="/playlists">
|
||||
<span class="icon"><i class="mdi mdi-library-music" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/music" v-if="is_visible_music">
|
||||
<span class="icon"><i class="mdi mdi-music"></i></span>
|
||||
<navbar-item-link v-if="is_visible_music" to="/music">
|
||||
<span class="icon"><i class="mdi mdi-music" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/podcasts" v-if="is_visible_podcasts">
|
||||
<span class="icon"><i class="mdi mdi-microphone"></i></span>
|
||||
<navbar-item-link v-if="is_visible_podcasts" to="/podcasts">
|
||||
<span class="icon"><i class="mdi mdi-microphone" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/audiobooks" v-if="is_visible_audiobooks">
|
||||
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span>
|
||||
<navbar-item-link v-if="is_visible_audiobooks" to="/audiobooks">
|
||||
<span class="icon"><i class="mdi mdi-book-open-variant" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/radio" v-if="is_visible_radio">
|
||||
<span class="icon"><i class="mdi mdi-radio"></i></span>
|
||||
<navbar-item-link v-if="is_visible_radio" to="/radio">
|
||||
<span class="icon"><i class="mdi mdi-radio" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/files" v-if="is_visible_files">
|
||||
<span class="icon"><i class="mdi mdi-folder-open"></i></span>
|
||||
<navbar-item-link v-if="is_visible_files" to="/files">
|
||||
<span class="icon"><i class="mdi mdi-folder-open" /></span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/search" v-if="is_visible_search">
|
||||
<span class="icon"><i class="mdi mdi-magnify"></i></span>
|
||||
<navbar-item-link v-if="is_visible_search" to="/search">
|
||||
<span class="icon"><i class="mdi mdi-magnify" /></span>
|
||||
</navbar-item-link>
|
||||
|
||||
<div class="navbar-burger" @click="show_burger_menu = !show_burger_menu" :class="{ 'is-active': show_burger_menu }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<div
|
||||
class="navbar-burger"
|
||||
:class="{ 'is-active': show_burger_menu }"
|
||||
@click="show_burger_menu = !show_burger_menu"
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu" :class="{ 'is-active': show_burger_menu }">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
<div class="navbar-start" />
|
||||
|
||||
<div class="navbar-end">
|
||||
|
||||
<!-- Burger menu entries -->
|
||||
<div class="navbar-item has-dropdown is-hoverable"
|
||||
:class="{ 'is-active': show_settings_menu }"
|
||||
@click="on_click_outside_settings">
|
||||
<div
|
||||
class="navbar-item has-dropdown is-hoverable"
|
||||
:class="{ 'is-active': show_settings_menu }"
|
||||
@click="on_click_outside_settings"
|
||||
>
|
||||
<a class="navbar-link is-arrowless">
|
||||
<span class="icon is-hidden-touch"><i class="mdi mdi-24px mdi-menu"></i></span>
|
||||
<span class="icon is-hidden-touch"
|
||||
><i class="mdi mdi-24px mdi-menu"
|
||||
/></span>
|
||||
<span class="is-hidden-desktop has-text-weight-bold">OwnTone</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-right">
|
||||
<navbar-item-link to="/playlists">
|
||||
<span class="icon"><i class="mdi mdi-library-music" /></span>
|
||||
<b>Playlists</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/music" exact>
|
||||
<span class="icon"><i class="mdi mdi-music" /></span>
|
||||
<b>Music</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/music/artists">
|
||||
<span class="fd-navbar-item-level2">Artists</span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/music/albums">
|
||||
<span class="fd-navbar-item-level2">Albums</span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/music/genres">
|
||||
<span class="fd-navbar-item-level2">Genres</span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link v-if="spotify_enabled" to="/music/spotify">
|
||||
<span class="fd-navbar-item-level2">Spotify</span>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/podcasts">
|
||||
<span class="icon"><i class="mdi mdi-microphone" /></span>
|
||||
<b>Podcasts</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/audiobooks">
|
||||
<span class="icon"><i class="mdi mdi-book-open-variant" /></span>
|
||||
<b>Audiobooks</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/radio">
|
||||
<span class="icon"><i class="mdi mdi-radio" /></span>
|
||||
<b>Radio</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/files">
|
||||
<span class="icon"><i class="mdi mdi-folder-open" /></span>
|
||||
<b>Files</b>
|
||||
</navbar-item-link>
|
||||
<navbar-item-link to="/search">
|
||||
<span class="icon"><i class="mdi mdi-magnify" /></span>
|
||||
<b>Search</b>
|
||||
</navbar-item-link>
|
||||
<hr class="fd-navbar-divider" />
|
||||
|
||||
<navbar-item-link to="/playlists"><span class="icon"><i class="mdi mdi-library-music"></i></span> <b>Playlists</b></navbar-item-link>
|
||||
<navbar-item-link to="/music" exact><span class="icon"><i class="mdi mdi-music"></i></span> <b>Music</b></navbar-item-link>
|
||||
<navbar-item-link to="/music/artists"><span class="fd-navbar-item-level2">Artists</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/albums"><span class="fd-navbar-item-level2">Albums</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/genres"><span class="fd-navbar-item-level2">Genres</span></navbar-item-link>
|
||||
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link>
|
||||
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link>
|
||||
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link>
|
||||
<navbar-item-link to="/radio"><span class="icon"><i class="mdi mdi-radio"></i></span> <b>Radio</b></navbar-item-link>
|
||||
<navbar-item-link to="/files"><span class="icon"><i class="mdi mdi-folder-open"></i></span> <b>Files</b></navbar-item-link>
|
||||
<navbar-item-link to="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link>
|
||||
<hr class="fd-navbar-divider">
|
||||
|
||||
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
|
||||
<a class="navbar-item" @click.stop.prevent="show_update_dialog = true; show_settings_menu = false; show_burger_menu = false">
|
||||
<navbar-item-link to="/settings/webinterface">
|
||||
Settings
|
||||
</navbar-item-link>
|
||||
<a class="navbar-item" @click.stop.prevent="open_update_dialog()">
|
||||
Update Library
|
||||
</a>
|
||||
<navbar-item-link to="/about">About</navbar-item-link>
|
||||
<navbar-item-link to="/about"> About </navbar-item-link>
|
||||
|
||||
<div class="navbar-item is-hidden-desktop" style="margin-bottom: 2.5rem;"></div>
|
||||
<div
|
||||
class="navbar-item is-hidden-desktop"
|
||||
style="margin-bottom: 2.5rem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="is-overlay" v-show="show_settings_menu"
|
||||
style="z-index:10; width: 100vw; height:100vh;"
|
||||
@click="show_settings_menu = false"></div>
|
||||
<div
|
||||
v-show="show_settings_menu"
|
||||
class="is-overlay"
|
||||
style="z-index: 10; width: 100vw; height: 100vh"
|
||||
@click="show_settings_menu = false"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavbarItemLink from './NavbarItemLink'
|
||||
import NavbarItemLink from './NavbarItemLink.vue'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'NavbarTop',
|
||||
components: { NavbarItemLink },
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
show_settings_menu: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_visible_playlists () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_playlists').value
|
||||
is_visible_playlists() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_playlists'
|
||||
).value
|
||||
},
|
||||
is_visible_music () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_music').value
|
||||
is_visible_music() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_music'
|
||||
).value
|
||||
},
|
||||
is_visible_podcasts () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_podcasts').value
|
||||
is_visible_podcasts() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_podcasts'
|
||||
).value
|
||||
},
|
||||
is_visible_audiobooks () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_audiobooks').value
|
||||
is_visible_audiobooks() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_audiobooks'
|
||||
).value
|
||||
},
|
||||
is_visible_radio () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_radio').value
|
||||
is_visible_radio() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_radio'
|
||||
).value
|
||||
},
|
||||
is_visible_files () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_files').value
|
||||
is_visible_files() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_files'
|
||||
).value
|
||||
},
|
||||
is_visible_search () {
|
||||
return this.$store.getters.settings_option('webinterface', 'show_menu_item_search').value
|
||||
is_visible_search() {
|
||||
return this.$store.getters.settings_option(
|
||||
'webinterface',
|
||||
'show_menu_item_search'
|
||||
).value
|
||||
},
|
||||
|
||||
player () {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
|
||||
config () {
|
||||
config() {
|
||||
return this.$store.state.config
|
||||
},
|
||||
|
||||
library () {
|
||||
library() {
|
||||
return this.$store.state.library
|
||||
},
|
||||
|
||||
audiobooks () {
|
||||
audiobooks() {
|
||||
return this.$store.state.audiobooks_count
|
||||
},
|
||||
|
||||
podcasts () {
|
||||
podcasts() {
|
||||
return this.$store.state.podcasts_count
|
||||
},
|
||||
|
||||
spotify_enabled () {
|
||||
spotify_enabled() {
|
||||
return this.$store.state.spotify.webapi_token_valid
|
||||
},
|
||||
|
||||
show_burger_menu: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_burger_menu
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, value)
|
||||
}
|
||||
},
|
||||
|
||||
show_player_menu () {
|
||||
show_player_menu() {
|
||||
return this.$store.state.show_player_menu
|
||||
},
|
||||
|
||||
show_update_dialog: {
|
||||
get () {
|
||||
get() {
|
||||
return this.$store.state.show_update_dialog
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
this.$store.commit(types.SHOW_UPDATE_DIALOG, value)
|
||||
}
|
||||
},
|
||||
|
||||
zindex () {
|
||||
zindex() {
|
||||
if (this.show_player_menu) {
|
||||
return 'z-index: 20'
|
||||
}
|
||||
@ -169,19 +237,24 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
on_click_outside_settings () {
|
||||
this.show_settings_menu = !this.show_settings_menu
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.show_settings_menu = false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
methods: {
|
||||
on_click_outside_settings() {
|
||||
this.show_settings_menu = !this.show_settings_menu
|
||||
},
|
||||
|
||||
open_update_dialog() {
|
||||
this.show_update_dialog = true
|
||||
this.show_settings_menu = false
|
||||
this.show_burger_menu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<section class="fd-notifications" v-if="notifications.length > 0">
|
||||
<section v-if="notifications.length > 0" class="fd-notifications">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<div class="notification has-shadow " v-for="notification in notifications" :key="notification.id" :class="['notification', notification.type ? `is-${notification.type}` : '']">
|
||||
<button class="delete" v-on:click="remove(notification)"></button>
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="notification has-shadow"
|
||||
:class="[
|
||||
'notification',
|
||||
notification.type ? `is-${notification.type}` : ''
|
||||
]"
|
||||
>
|
||||
<button class="delete" @click="remove(notification)" />
|
||||
{{ notification.text }}
|
||||
</div>
|
||||
</div>
|
||||
@ -15,15 +23,15 @@
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
components: { },
|
||||
name: 'NotificationList',
|
||||
components: {},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return { showNav: false }
|
||||
},
|
||||
|
||||
computed: {
|
||||
notifications () {
|
||||
notifications() {
|
||||
return this.$store.state.notifications.list
|
||||
}
|
||||
},
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a @click="toggle_consume_mode" :class="{ 'is-warning': is_consume }">
|
||||
<span class="icon"><i class="mdi mdi-fire" :class="icon_style"></i></span>
|
||||
<a :class="{ 'is-warning': is_consume }" @click="toggle_consume_mode">
|
||||
<span class="icon"><i class="mdi mdi-fire" :class="icon_style" /></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,7 +15,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_consume () {
|
||||
is_consume() {
|
||||
return this.$store.state.player.consume
|
||||
}
|
||||
},
|
||||
@ -28,5 +28,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<a @click="play_next" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-forward" :class="icon_style"></i></span>
|
||||
<a :disabled="disabled" @click="play_next">
|
||||
<span class="icon"
|
||||
><i class="mdi mdi-skip-forward" :class="icon_style"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,7 +17,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
disabled() {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0
|
||||
}
|
||||
},
|
||||
@ -32,5 +34,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<a @click="toggle_play_pause" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing && is_pause_allowed, 'mdi-stop': is_playing && !is_pause_allowed }]"></i></span>
|
||||
<a :disabled="disabled" @click="toggle_play_pause">
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi"
|
||||
:class="[
|
||||
icon_style,
|
||||
{
|
||||
'mdi-play': !is_playing,
|
||||
'mdi-pause': is_playing && is_pause_allowed,
|
||||
'mdi-stop': is_playing && !is_pause_allowed
|
||||
}
|
||||
]"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -16,16 +27,18 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_playing () {
|
||||
is_playing() {
|
||||
return this.$store.state.player.state === 'play'
|
||||
},
|
||||
|
||||
is_pause_allowed () {
|
||||
return (this.$store.getters.now_playing &&
|
||||
this.$store.getters.now_playing.data_kind !== 'pipe')
|
||||
is_pause_allowed() {
|
||||
return (
|
||||
this.$store.getters.now_playing &&
|
||||
this.$store.getters.now_playing.data_kind !== 'pipe'
|
||||
)
|
||||
},
|
||||
|
||||
disabled () {
|
||||
disabled() {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0
|
||||
}
|
||||
},
|
||||
@ -34,7 +47,12 @@ export default {
|
||||
toggle_play_pause: function () {
|
||||
if (this.disabled) {
|
||||
if (this.show_disabled_message) {
|
||||
this.$store.dispatch('add_notification', { text: 'Queue is empty', type: 'info', topic: 'connection', timeout: 2000 })
|
||||
this.$store.dispatch('add_notification', {
|
||||
text: 'Queue is empty',
|
||||
type: 'info',
|
||||
topic: 'connection',
|
||||
timeout: 2000
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -51,5 +69,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<a @click="play_previous" :disabled="disabled">
|
||||
<span class="icon"><i class="mdi mdi-skip-backward" :class="icon_style"></i></span>
|
||||
<a :disabled="disabled" @click="play_previous">
|
||||
<span class="icon"
|
||||
><i class="mdi mdi-skip-backward" :class="icon_style"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,7 +17,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled () {
|
||||
disabled() {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0
|
||||
}
|
||||
},
|
||||
@ -32,5 +34,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<a @click="toggle_repeat_mode" :class="{ 'is-warning': !is_repeat_off }">
|
||||
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-repeat': is_repeat_all, 'mdi-repeat-once': is_repeat_single, 'mdi-repeat-off': is_repeat_off }]"></i></span>
|
||||
<a :class="{ 'is-warning': !is_repeat_off }" @click="toggle_repeat_mode">
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi"
|
||||
:class="[
|
||||
icon_style,
|
||||
{
|
||||
'mdi-repeat': is_repeat_all,
|
||||
'mdi-repeat-once': is_repeat_single,
|
||||
'mdi-repeat-off': is_repeat_off
|
||||
}
|
||||
]"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,13 +26,13 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_repeat_all () {
|
||||
is_repeat_all() {
|
||||
return this.$store.state.player.repeat === 'all'
|
||||
},
|
||||
is_repeat_single () {
|
||||
is_repeat_single() {
|
||||
return this.$store.state.player.repeat === 'single'
|
||||
},
|
||||
is_repeat_off () {
|
||||
is_repeat_off() {
|
||||
return !this.is_repeat_all && !this.is_repeat_single
|
||||
}
|
||||
},
|
||||
@ -40,5 +51,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||
<span class="icon"><i class="mdi mdi-rewind" :class="icon_style"></i></span>
|
||||
<a v-if="visible" :disabled="disabled" @click="seek">
|
||||
<span class="icon"><i class="mdi mdi-rewind" :class="icon_style" /></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -12,17 +12,21 @@ export default {
|
||||
props: ['seek_ms', 'icon_style'],
|
||||
|
||||
computed: {
|
||||
now_playing () {
|
||||
now_playing() {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
is_stopped () {
|
||||
is_stopped() {
|
||||
return this.$store.state.player.state === 'stop'
|
||||
},
|
||||
disabled () {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped ||
|
||||
this.now_playing.data_kind === 'pipe'
|
||||
disabled() {
|
||||
return (
|
||||
!this.$store.state.queue ||
|
||||
this.$store.state.queue.count <= 0 ||
|
||||
this.is_stopped ||
|
||||
this.now_playing.data_kind === 'pipe'
|
||||
)
|
||||
},
|
||||
visible () {
|
||||
visible() {
|
||||
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||
<span class="icon"><i class="mdi mdi-fast-forward" :class="icon_style"></i></span>
|
||||
<a v-if="visible" :disabled="disabled" @click="seek">
|
||||
<span class="icon"
|
||||
><i class="mdi mdi-fast-forward" :class="icon_style"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -12,17 +14,21 @@ export default {
|
||||
props: ['seek_ms', 'icon_style'],
|
||||
|
||||
computed: {
|
||||
now_playing () {
|
||||
now_playing() {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
is_stopped () {
|
||||
is_stopped() {
|
||||
return this.$store.state.player.state === 'stop'
|
||||
},
|
||||
disabled () {
|
||||
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped ||
|
||||
this.now_playing.data_kind === 'pipe'
|
||||
disabled() {
|
||||
return (
|
||||
!this.$store.state.queue ||
|
||||
this.$store.state.queue.count <= 0 ||
|
||||
this.is_stopped ||
|
||||
this.now_playing.data_kind === 'pipe'
|
||||
)
|
||||
},
|
||||
visible () {
|
||||
visible() {
|
||||
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<a @click="toggle_shuffle_mode" :class="{ 'is-warning': is_shuffle }">
|
||||
<span class="icon"><i class="mdi" :class="[icon_style, { 'mdi-shuffle': is_shuffle, 'mdi-shuffle-disabled': !is_shuffle }]"></i></span>
|
||||
<a :class="{ 'is-warning': is_shuffle }" @click="toggle_shuffle_mode">
|
||||
<span class="icon"
|
||||
><i
|
||||
class="mdi"
|
||||
:class="[
|
||||
icon_style,
|
||||
{ 'mdi-shuffle': is_shuffle, 'mdi-shuffle-disabled': !is_shuffle }
|
||||
]"
|
||||
/></span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -15,7 +22,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
is_shuffle () {
|
||||
is_shuffle() {
|
||||
return this.$store.state.player.shuffle
|
||||
}
|
||||
},
|
||||
@ -28,5 +35,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
26
web-src/src/components/ProgressBar.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<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>
|
@ -1,19 +1,25 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
:checked="value"
|
||||
@change="set_update_timer"
|
||||
ref="settings_checkbox">
|
||||
<slot name="label"></slot>
|
||||
<i class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"> {{ info }}</i>
|
||||
<input
|
||||
ref="settings_checkbox"
|
||||
type="checkbox"
|
||||
:checked="value"
|
||||
@change="set_update_timer"
|
||||
/>
|
||||
<slot name="label" />
|
||||
<i
|
||||
class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"
|
||||
>
|
||||
{{ info }}</i
|
||||
>
|
||||
</label>
|
||||
<p class="help" v-if="$slots['info']">
|
||||
<slot name="info"></slot>
|
||||
<p v-if="$slots['info']" class="help">
|
||||
<slot name="info" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -27,7 +33,7 @@ export default {
|
||||
|
||||
props: ['category_name', 'option_name'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerId: -1,
|
||||
@ -38,22 +44,26 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
category () {
|
||||
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
|
||||
category() {
|
||||
return this.$store.state.settings.categories.find(
|
||||
(elem) => elem.name === this.category_name
|
||||
)
|
||||
},
|
||||
|
||||
option () {
|
||||
option() {
|
||||
if (!this.category) {
|
||||
return {}
|
||||
}
|
||||
return this.category.options.find(elem => elem.name === this.option_name)
|
||||
return this.category.options.find(
|
||||
(elem) => elem.name === this.option_name
|
||||
)
|
||||
},
|
||||
|
||||
value () {
|
||||
value() {
|
||||
return this.option.value
|
||||
},
|
||||
|
||||
info () {
|
||||
info() {
|
||||
if (this.statusUpdate === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdate === 'error') {
|
||||
@ -64,7 +74,7 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_update_timer () {
|
||||
set_update_timer() {
|
||||
if (this.timerId > 0) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = -1
|
||||
@ -77,10 +87,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
update_setting () {
|
||||
update_setting() {
|
||||
this.timerId = -1
|
||||
|
||||
const newValue = this.$refs.settings_checkbox.checked
|
||||
console.log(this.$refs.settings_checkbox)
|
||||
if (newValue === this.value) {
|
||||
this.statusUpdate = ''
|
||||
return
|
||||
@ -91,15 +102,19 @@ export default {
|
||||
name: this.option_name,
|
||||
value: newValue
|
||||
}
|
||||
webapi.settings_update(this.category.name, option).then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
}).catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_checkbox.checked = this.value
|
||||
}).finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
webapi
|
||||
.settings_update(this.category.name, option)
|
||||
.then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
})
|
||||
.catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_checkbox.checked = this.value
|
||||
})
|
||||
.finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status: function () {
|
||||
@ -109,5 +124,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -2,25 +2,31 @@
|
||||
<fieldset :disabled="disabled">
|
||||
<div class="field">
|
||||
<label class="label has-text-weight-normal">
|
||||
<slot name="label"></slot>
|
||||
<i class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"> {{ info }}</i>
|
||||
<slot name="label" />
|
||||
<i
|
||||
class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"
|
||||
>
|
||||
{{ info }}</i
|
||||
>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
style="width: 10em;"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="set_update_timer"
|
||||
ref="settings_number">
|
||||
<input
|
||||
ref="settings_number"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
style="width: 10em"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="set_update_timer"
|
||||
/>
|
||||
</div>
|
||||
<p class="help" v-if="$slots['info']">
|
||||
<slot name="info"></slot>
|
||||
<p v-if="$slots['info']" class="help">
|
||||
<slot name="info" />
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
@ -35,7 +41,7 @@ export default {
|
||||
|
||||
props: ['category_name', 'option_name', 'placeholder', 'disabled'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerId: -1,
|
||||
@ -45,22 +51,26 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
category () {
|
||||
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
|
||||
category() {
|
||||
return this.$store.state.settings.categories.find(
|
||||
(elem) => elem.name === this.category_name
|
||||
)
|
||||
},
|
||||
|
||||
option () {
|
||||
option() {
|
||||
if (!this.category) {
|
||||
return {}
|
||||
}
|
||||
return this.category.options.find(elem => elem.name === this.option_name)
|
||||
return this.category.options.find(
|
||||
(elem) => elem.name === this.option_name
|
||||
)
|
||||
},
|
||||
|
||||
value () {
|
||||
value() {
|
||||
return this.option.value
|
||||
},
|
||||
|
||||
info () {
|
||||
info() {
|
||||
if (this.statusUpdate === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdate === 'error') {
|
||||
@ -71,7 +81,7 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_update_timer () {
|
||||
set_update_timer() {
|
||||
if (this.timerId > 0) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = -1
|
||||
@ -84,7 +94,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
update_setting () {
|
||||
update_setting() {
|
||||
this.timerId = -1
|
||||
|
||||
const newValue = this.$refs.settings_number.value
|
||||
@ -98,15 +108,19 @@ export default {
|
||||
name: this.option_name,
|
||||
value: parseInt(newValue, 10)
|
||||
}
|
||||
webapi.settings_update(this.category.name, option).then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
}).catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_number.value = this.value
|
||||
}).finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
webapi
|
||||
.settings_update(this.category.name, option)
|
||||
.then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
})
|
||||
.catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_number.value = this.value
|
||||
})
|
||||
.finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status: function () {
|
||||
@ -116,5 +130,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -2,21 +2,29 @@
|
||||
<fieldset :disabled="disabled">
|
||||
<div class="field">
|
||||
<label class="label has-text-weight-normal">
|
||||
<slot name="label"></slot>
|
||||
<i class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"> {{ info }}</i>
|
||||
<slot name="label" />
|
||||
<i
|
||||
class="is-size-7"
|
||||
:class="{
|
||||
'has-text-info': statusUpdate === 'success',
|
||||
'has-text-danger': statusUpdate === 'error'
|
||||
}"
|
||||
>
|
||||
{{ info }}</i
|
||||
>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" :placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="set_update_timer"
|
||||
ref="settings_text">
|
||||
<input
|
||||
ref="settings_text"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="set_update_timer"
|
||||
/>
|
||||
</div>
|
||||
<p class="help" v-if="$slots['info']">
|
||||
<slot name="info"></slot>
|
||||
<p v-if="$slots['info']" class="help">
|
||||
<slot name="info" />
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
@ -31,7 +39,7 @@ export default {
|
||||
|
||||
props: ['category_name', 'option_name', 'placeholder', 'disabled'],
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
timerDelay: 2000,
|
||||
timerId: -1,
|
||||
@ -42,22 +50,26 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
category () {
|
||||
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
|
||||
category() {
|
||||
return this.$store.state.settings.categories.find(
|
||||
(elem) => elem.name === this.category_name
|
||||
)
|
||||
},
|
||||
|
||||
option () {
|
||||
option() {
|
||||
if (!this.category) {
|
||||
return {}
|
||||
}
|
||||
return this.category.options.find(elem => elem.name === this.option_name)
|
||||
return this.category.options.find(
|
||||
(elem) => elem.name === this.option_name
|
||||
)
|
||||
},
|
||||
|
||||
value () {
|
||||
value() {
|
||||
return this.option.value
|
||||
},
|
||||
|
||||
info () {
|
||||
info() {
|
||||
if (this.statusUpdate === 'success') {
|
||||
return '(setting saved)'
|
||||
} else if (this.statusUpdate === 'error') {
|
||||
@ -68,7 +80,7 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
set_update_timer () {
|
||||
set_update_timer() {
|
||||
if (this.timerId > 0) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = -1
|
||||
@ -81,7 +93,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
update_setting () {
|
||||
update_setting() {
|
||||
this.timerId = -1
|
||||
|
||||
const newValue = this.$refs.settings_text.value
|
||||
@ -95,15 +107,19 @@ export default {
|
||||
name: this.option_name,
|
||||
value: newValue
|
||||
}
|
||||
webapi.settings_update(this.category.name, option).then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
}).catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_text.value = this.value
|
||||
}).finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
webapi
|
||||
.settings_update(this.category.name, option)
|
||||
.then(() => {
|
||||
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
|
||||
this.statusUpdate = 'success'
|
||||
})
|
||||
.catch(() => {
|
||||
this.statusUpdate = 'error'
|
||||
this.$refs.settings_text.value = this.value
|
||||
})
|
||||
.finally(() => {
|
||||
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
|
||||
})
|
||||
},
|
||||
|
||||
clear_status: function () {
|
||||
@ -113,5 +129,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,17 +1,21 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<div class="media">
|
||||
<div class="media-left fd-has-action"
|
||||
v-if="$slots['artwork']"
|
||||
@click="listeners.click">
|
||||
<slot name="artwork"></slot>
|
||||
<div v-if="$slots['artwork']" class="media-left fd-has-action">
|
||||
<slot name="artwork" />
|
||||
</div>
|
||||
<div class="media-content fd-has-action is-clipped" @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>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,5 +27,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div class="media">
|
||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist">
|
||||
<h1 class="title is-6">{{ artist.name }}</h1>
|
||||
<div class="media-content fd-has-action is-clipped" @click="open_artist">
|
||||
<h1 class="title is-6">
|
||||
{{ artist.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -22,5 +24,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<div class="media">
|
||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_playlist">
|
||||
<h1 class="title is-6">{{ playlist.name }}</h1>
|
||||
<h2 class="subtitle is-7">{{ playlist.owner.display_name }}</h2>
|
||||
<div class="media-content fd-has-action is-clipped" @click="open_playlist">
|
||||
<h1 class="title is-6">
|
||||
{{ playlist.name }}
|
||||
</h1>
|
||||
<h2 class="subtitle is-7">
|
||||
{{ playlist.owner.display_name }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -17,11 +21,12 @@ export default {
|
||||
|
||||
methods: {
|
||||
open_playlist: function () {
|
||||
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id })
|
||||
this.$router.push({
|
||||
path: '/music/spotify/playlists/' + this.playlist.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|