mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 07:35:57 -05:00
Merge pull request #617 from chme/webinterface
Update player web interface
This commit is contained in:
commit
0d8a764f41
@ -82,28 +82,28 @@ htdocsplayercssdir = $(datadir)/forked-daapd/htdocs/player/css
|
||||
|
||||
dist_htdocsplayercss_DATA = \
|
||||
player/css/app.css \
|
||||
player/css/app.css.map
|
||||
player/css/app.css.map \
|
||||
player/css/chunk-vendors.css \
|
||||
player/css/chunk-vendors.css.map
|
||||
|
||||
htdocsplayerfontsdir = $(datadir)/forked-daapd/htdocs/player/fonts
|
||||
|
||||
dist_htdocsplayerfonts_DATA = \
|
||||
player/fonts/materialdesignicons-webfont.13621ea.ttf \
|
||||
player/fonts/materialdesignicons-webfont.5cdddea.woff2 \
|
||||
player/fonts/materialdesignicons-webfont.1bd36f0.woff \
|
||||
player/fonts/materialdesignicons-webfont.dbcb3fe.eot
|
||||
player/fonts/materialdesignicons-webfont.ttf \
|
||||
player/fonts/materialdesignicons-webfont.woff2 \
|
||||
player/fonts/materialdesignicons-webfont.woff \
|
||||
player/fonts/materialdesignicons-webfont.eot
|
||||
|
||||
htdocsplayerjsdir = $(datadir)/forked-daapd/htdocs/player/js
|
||||
|
||||
dist_htdocsplayerjs_DATA = \
|
||||
player/js/app.js \
|
||||
player/js/app.js.map \
|
||||
player/js/manifest.js \
|
||||
player/js/manifest.js.map \
|
||||
player/js/vendor.js \
|
||||
player/js/vendor.js.map
|
||||
player/js/chunk-vendors.js \
|
||||
player/js/chunk-vendors.js.map
|
||||
|
||||
htdocsplayerimgdir = $(datadir)/forked-daapd/htdocs/player/img
|
||||
|
||||
dist_htdocsplayerimg_DATA = \
|
||||
player/img/materialdesignicons-webfont.55a80a2.svg
|
||||
player/img/materialdesignicons-webfont.svg
|
||||
endif
|
||||
|
@ -1 +1 @@
|
||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web</title><link rel=apple-touch-icon sizes=120x120 href=/apple-touch-icon.png><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=stylesheet></head><body><div id=app></div><script type=text/javascript src=/player/js/manifest.js></script><script type=text/javascript src=/player/js/vendor.js></script><script type=text/javascript src=/player/js/app.js></script></body></html>
|
||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web 2</title><link rel=apple-touch-icon sizes=120x120 href=/apple-touch-icon.png><link rel=icon type=image/png sizes=32x32 href=/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/favicon-16x16.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/safari-pinned-tab.svg color=#5bbad5><meta name=msapplication-TileColor content=#da532c><meta name=theme-color content=#ffffff><link href=/player/css/app.css rel=preload as=style><link href=/player/css/chunk-vendors.css rel=preload as=style><link href=/player/js/app.js rel=preload as=script><link href=/player/js/chunk-vendors.js rel=preload as=script><link href=/player/css/chunk-vendors.css rel=stylesheet><link href=/player/css/app.css rel=stylesheet></head><body><div id=app></div><script src=/player/js/chunk-vendors.js></script><script src=/player/js/app.js></script></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
htdocs/player/css/chunk-vendors.css
Normal file
4
htdocs/player/css/chunk-vendors.css
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/player/css/chunk-vendors.css.map
Normal file
1
htdocs/player/css/chunk-vendors.css.map
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
htdocs/player/fonts/materialdesignicons-webfont.woff
Normal file
BIN
htdocs/player/fonts/materialdesignicons-webfont.woff
Normal file
Binary file not shown.
BIN
htdocs/player/fonts/materialdesignicons-webfont.woff2
Normal file
BIN
htdocs/player/fonts/materialdesignicons-webfont.woff2
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
72
htdocs/player/js/chunk-vendors.js
Normal file
72
htdocs/player/js/chunk-vendors.js
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/player/js/chunk-vendors.js.map
Normal file
1
htdocs/player/js/chunk-vendors.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
||||
!function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,p,i,a=0,l=[];a<e.length;a++)p=e[a],o[p]&&l.push(o[p][0]),o[p]=0;for(f in u)Object.prototype.hasOwnProperty.call(u,f)&&(r[f]=u[f]);for(n&&n(e,u,c);l.length;)l.shift()();if(c)for(a=0;a<c.length;a++)i=t(t.s=c[a]);return i};var e={},o={2:0};function t(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return r[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=r,t.c=e,t.d=function(r,n,e){t.o(r,n)||Object.defineProperty(r,n,{configurable:!1,enumerable:!0,get:e})},t.n=function(r){var n=r&&r.__esModule?function(){return r.default}:function(){return r};return t.d(n,"a",n),n},t.o=function(r,n){return Object.prototype.hasOwnProperty.call(r,n)},t.p="/player/",t.oe=function(r){throw console.error(r),r}}([]);
|
||||
//# sourceMappingURL=manifest.js.map
|
@ -1 +0,0 @@
|
||||
{"version":3,"sources":["webpack:///webpack/bootstrap 5b6ad3b17696a3563194"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,WAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"js/manifest.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/player/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 5b6ad3b17696a3563194"],"sourceRoot":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -82,6 +82,9 @@ STRTAG : 'artist'
|
||||
| 'path'
|
||||
| 'type'
|
||||
| 'grouping'
|
||||
| 'artist_id'
|
||||
| 'album_id'
|
||||
| 'songartistid'
|
||||
;
|
||||
|
||||
INTTAG : 'play_count'
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-vue-jsx", "transform-runtime"]
|
||||
}
|
3
web-src/.browserslistrc
Normal file
3
web-src/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
@ -1,9 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
@ -1,4 +0,0 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
@ -1,29 +1,17 @@
|
||||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
extends: [
|
||||
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
||||
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
||||
'plugin:vue/essential',
|
||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
||||
'standard'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow async-await
|
||||
'generator-star-spacing': 'off',
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
"postcss-url": {},
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
5
web-src/babel.config.js
Normal file
5
web-src/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
'use strict'
|
||||
require('./check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
const ora = require('ora')
|
||||
const rm = require('rimraf')
|
||||
const path = require('path')
|
||||
const chalk = require('chalk')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
const spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
||||
if (err) throw err
|
||||
webpack(webpackConfig, (err, stats) => {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(chalk.red(' Build failed with errors.\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
console.log(chalk.yellow(
|
||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
))
|
||||
})
|
||||
})
|
@ -1,54 +0,0 @@
|
||||
'use strict'
|
||||
const chalk = require('chalk')
|
||||
const semver = require('semver')
|
||||
const packageConfig = require('../package.json')
|
||||
const shell = require('shelljs')
|
||||
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
const versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
]
|
||||
|
||||
if (shell.which('npm')) {
|
||||
versionRequirements.push({
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
const warnings = []
|
||||
|
||||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
@ -1,101 +0,0 @@
|
||||
'use strict'
|
||||
const path = require('path')
|
||||
const config = require('../config')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const packageConfig = require('../package.json')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
const assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
const cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
const postcssLoader = {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loader, loaderOptions) {
|
||||
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
|
||||
|
||||
if (loader) {
|
||||
loaders.push({
|
||||
loader: loader + '-loader',
|
||||
options: Object.assign({}, loaderOptions, {
|
||||
sourceMap: options.sourceMap
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
less: generateLoaders('less'),
|
||||
sass: generateLoaders('sass', { indentedSyntax: true }),
|
||||
scss: generateLoaders('sass'),
|
||||
stylus: generateLoaders('stylus'),
|
||||
styl: generateLoaders('stylus')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
const output = []
|
||||
const loaders = exports.cssLoaders(options)
|
||||
|
||||
for (const extension in loaders) {
|
||||
const loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
exports.createNotifierCallback = () => {
|
||||
const notifier = require('node-notifier')
|
||||
|
||||
return (severity, errors) => {
|
||||
if (severity !== 'error') return
|
||||
|
||||
const error = errors[0]
|
||||
const filename = error.file && error.file.split('!').pop()
|
||||
|
||||
notifier.notify({
|
||||
title: packageConfig.name,
|
||||
message: severity + ': ' + error.name,
|
||||
subtitle: filename || '',
|
||||
icon: path.join(__dirname, 'logo.png')
|
||||
})
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
'use strict'
|
||||
const utils = require('./utils')
|
||||
const config = require('../config')
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const sourceMapEnabled = isProduction
|
||||
? config.build.productionSourceMap
|
||||
: config.dev.cssSourceMap
|
||||
|
||||
module.exports = {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: sourceMapEnabled,
|
||||
extract: isProduction
|
||||
}),
|
||||
cssSourceMap: sourceMapEnabled,
|
||||
cacheBusting: config.dev.cacheBusting,
|
||||
transformToRequire: {
|
||||
video: ['src', 'poster'],
|
||||
source: 'src',
|
||||
img: 'src',
|
||||
image: 'xlink:href'
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const config = require('../config')
|
||||
const vueLoaderConfig = require('./vue-loader.conf')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
const createLintingRule = () => ({
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
emitWarning: !config.dev.showEslintErrorsInOverlay
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
context: path.resolve(__dirname, '../'),
|
||||
entry: {
|
||||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'@': resolve('src'),
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
...(config.dev.useEslint ? [createLintingRule()] : []),
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('media/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
// prevent webpack from injecting useless setImmediate polyfill because Vue
|
||||
// source contains it (although only uses it if it's native).
|
||||
setImmediate: false,
|
||||
// prevent webpack from injecting mocks to Node native modules
|
||||
// that does not make sense for the client
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
'use strict'
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const merge = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const portfinder = require('portfinder')
|
||||
|
||||
const HOST = process.env.HOST
|
||||
const PORT = process.env.PORT && Number(process.env.PORT)
|
||||
|
||||
const devWebpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: config.dev.devtool,
|
||||
|
||||
// these devServer options should be customized in /config/index.js
|
||||
devServer: {
|
||||
clientLogLevel: 'warning',
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
|
||||
],
|
||||
},
|
||||
hot: true,
|
||||
contentBase: false, // since we use CopyWebpackPlugin.
|
||||
compress: true,
|
||||
host: HOST || config.dev.host,
|
||||
port: PORT || config.dev.port,
|
||||
open: config.dev.autoOpenBrowser,
|
||||
overlay: config.dev.errorOverlay
|
||||
? { warnings: false, errors: true }
|
||||
: false,
|
||||
publicPath: config.dev.assetsPublicPath,
|
||||
proxy: config.dev.proxyTable,
|
||||
quiet: true, // necessary for FriendlyErrorsPlugin
|
||||
watchOptions: {
|
||||
poll: config.dev.poll,
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../config/dev.env')
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.dev.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = new Promise((resolve, reject) => {
|
||||
portfinder.basePort = process.env.PORT || config.dev.port
|
||||
portfinder.getPort((err, port) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
// publish the new Port, necessary for e2e tests
|
||||
process.env.PORT = port
|
||||
// add port to devServer config
|
||||
devWebpackConfig.devServer.port = port
|
||||
|
||||
// Add FriendlyErrorsPlugin
|
||||
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
|
||||
compilationSuccessInfo: {
|
||||
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
|
||||
},
|
||||
onErrors: config.dev.notifyOnErrors
|
||||
? utils.createNotifierCallback()
|
||||
: undefined
|
||||
}))
|
||||
|
||||
resolve(devWebpackConfig)
|
||||
}
|
||||
})
|
||||
})
|
@ -1,145 +0,0 @@
|
||||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||
|
||||
const env = require('../config/prod.env')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true,
|
||||
usePostCSS: true
|
||||
})
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? config.build.devtool : false,
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
}),
|
||||
new UglifyJsPlugin({
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
},
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
parallel: true
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].css'),
|
||||
// Setting the following option to `false` will not extract CSS from codesplit chunks.
|
||||
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
|
||||
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
|
||||
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
|
||||
allChunks: true,
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: config.build.productionSourceMap
|
||||
? { safe: true, map: { inline: false } }
|
||||
: { safe: true }
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// keep module.id stable when vendor modules does not change
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
// enable scope hoisting
|
||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
// split vendor js into its own file
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks (module) {
|
||||
// any required modules inside node_modules are extracted to vendor
|
||||
return (
|
||||
module.resource &&
|
||||
/\.js$/.test(module.resource) &&
|
||||
module.resource.indexOf(
|
||||
path.join(__dirname, '../node_modules')
|
||||
) === 0
|
||||
)
|
||||
}
|
||||
}),
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
minChunks: Infinity
|
||||
}),
|
||||
// This instance extracts shared chunks from code splitted chunks and bundles them
|
||||
// in a separate chunk, similar to the vendor chunk
|
||||
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'app',
|
||||
async: 'vendor-async',
|
||||
children: true,
|
||||
minChunks: 3
|
||||
}),
|
||||
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.build.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
@ -1,7 +0,0 @@
|
||||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
@ -1,83 +0,0 @@
|
||||
'use strict'
|
||||
// Template version: 1.3.1
|
||||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
dev: {
|
||||
|
||||
// Paths
|
||||
assetsSubDirectory: '',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
// proxy all requests starting with /api to local forked-daapd instance
|
||||
'/api': {
|
||||
target: 'http://localhost:3689',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { }
|
||||
}
|
||||
},
|
||||
|
||||
// Various Dev Server settings
|
||||
host: 'localhost', // can be overwritten by process.env.HOST
|
||||
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
|
||||
autoOpenBrowser: false,
|
||||
errorOverlay: true,
|
||||
notifyOnErrors: true,
|
||||
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
|
||||
|
||||
// Use Eslint Loader?
|
||||
// If true, your code will be linted during bundling and
|
||||
// linting errors and warnings will be shown in the console.
|
||||
useEslint: true,
|
||||
// If true, eslint errors and warnings will also be shown in the error overlay
|
||||
// in the browser.
|
||||
showEslintErrorsInOverlay: false,
|
||||
|
||||
/**
|
||||
* Source Maps
|
||||
*/
|
||||
|
||||
// https://webpack.js.org/configuration/devtool/#development
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
|
||||
// If you have problems debugging vue-files in devtools,
|
||||
// set this to false - it *may* help
|
||||
// https://vue-loader.vuejs.org/en/options.html#cachebusting
|
||||
cacheBusting: true,
|
||||
|
||||
cssSourceMap: true
|
||||
},
|
||||
|
||||
build: {
|
||||
// Template for index.html
|
||||
index: path.resolve(__dirname, '../../htdocs/index.html'),
|
||||
|
||||
// Paths
|
||||
assetsRoot: path.resolve(__dirname, '../../htdocs/player'),
|
||||
assetsSubDirectory: '',
|
||||
assetsPublicPath: '/player/',
|
||||
|
||||
/**
|
||||
* Source Maps
|
||||
*/
|
||||
|
||||
productionSourceMap: true,
|
||||
// https://webpack.js.org/configuration/devtool/#production
|
||||
devtool: '#source-map',
|
||||
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css'],
|
||||
|
||||
// Run the build command with an extra argument to
|
||||
// View the bundle analyzer report after build finishes:
|
||||
// `npm run build --report`
|
||||
// Set to `true` or `false` to always turn it on or off
|
||||
bundleAnalyzerReport: process.env.npm_config_report
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
'use strict'
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"',
|
||||
VERSION: '"123"',
|
||||
V2: JSON.stringify(require("../package.json").version)
|
||||
}
|
18785
web-src/package-lock.json
generated
18785
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "forked-daapd-web",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "forked-daapd web interface",
|
||||
"author": "chme <christian.meffert@googlemail.com>",
|
||||
"license": "GPL-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
||||
"start": "npm run dev",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"build": "node build/build.js"
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --no-clean",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
@ -17,72 +17,22 @@
|
||||
"mdi": "^2.1.99",
|
||||
"moment": "^2.22.1",
|
||||
"moment-duration-format": "^2.2.2",
|
||||
"npm": "^5.8.0",
|
||||
"npm": "^6.4.1",
|
||||
"reconnectingwebsocket": "^1.0.0",
|
||||
"spotify-web-api-js": "^0.23.0",
|
||||
"vue": "^2.5.16",
|
||||
"vue-infinite-loading": "^2.2.3",
|
||||
"spotify-web-api-js": "^1.1.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-infinite-loading": "^2.4.0",
|
||||
"vue-progressbar": "^0.7.4",
|
||||
"vue-range-slider": "^0.6.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue2-touch-events": "^1.0.0",
|
||||
"vuedraggable": "^2.16.0",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.2.6",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-plugin-transform-vue-jsx": "^3.7.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"chalk": "^2.3.2",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-import": "^2.9.0",
|
||||
"eslint-plugin-node": "^5.2.0",
|
||||
"eslint-plugin-promise": "^3.7.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"eslint-plugin-vue": "^4.4.0",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"node-notifier": "^5.1.2",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"ora": "^1.4.0",
|
||||
"portfinder": "^1.0.13",
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-loader": "^2.1.3",
|
||||
"postcss-url": "^7.3.1",
|
||||
"rimraf": "^2.6.0",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^13.7.1",
|
||||
"vue-style-loader": "^3.1.2",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^3.11.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-dev-server": "^2.11.2",
|
||||
"webpack-merge": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
"@vue/cli-plugin-babel": "^3.1.1",
|
||||
"@vue/cli-plugin-eslint": "^3.1.4",
|
||||
"@vue/cli-service": "^3.1.2",
|
||||
"@vue/eslint-config-standard": "^3.0.5",
|
||||
"vue-template-compiler": "^2.5.17"
|
||||
}
|
||||
}
|
||||
|
5
web-src/postcss.config.js
Normal file
5
web-src/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>forked-daapd-web</title>
|
||||
<title>forked-daapd-web 2</title>
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
@ -25,7 +25,8 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
token_timer_id: 0
|
||||
token_timer_id: 0,
|
||||
reconnect_attempts: 0
|
||||
}
|
||||
},
|
||||
|
||||
@ -88,11 +89,12 @@ export default {
|
||||
var socket = new ReconnectingWebSocket(
|
||||
'ws://' + window.location.hostname + ':' + vm.$store.state.config.websocket_port,
|
||||
'notify',
|
||||
{ reconnectInterval: 5000 }
|
||||
{ reconnectInterval: 3000 }
|
||||
)
|
||||
|
||||
socket.onopen = function () {
|
||||
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', 'player', 'options', 'outputs', 'volume', 'spotify'] }))
|
||||
|
||||
vm.update_outputs()
|
||||
@ -105,7 +107,8 @@ export default {
|
||||
// vm.$store.dispatch('add_notification', { text: 'Connection closed', type: 'danger', timeout: 2000 })
|
||||
}
|
||||
socket.onerror = function () {
|
||||
vm.$store.dispatch('add_notification', { text: 'Connection lost. Reconnecting ...', type: 'danger', topic: 'connection' })
|
||||
vm.reconnect_attempts++
|
||||
vm.$store.dispatch('add_notification', { text: 'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')', type: 'danger', topic: 'connection' })
|
||||
}
|
||||
socket.onmessage = function (response) {
|
||||
var data = JSON.parse(response.data)
|
||||
|
@ -34,6 +34,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -64,11 +67,7 @@ export default {
|
||||
methods: {
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.album.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -78,6 +77,13 @@ export default {
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_add_next(this.album.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
},
|
||||
|
||||
open_album: function () {
|
||||
this.show_details_modal = false
|
||||
if (this.media_kind === 'podcast') {
|
||||
|
@ -29,6 +29,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -59,11 +62,7 @@ export default {
|
||||
methods: {
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.artist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.artist.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -73,6 +72,13 @@ export default {
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_add_next(this.artist.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
},
|
||||
|
||||
open_artist: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/music/artists/' + this.artist.id })
|
||||
|
99
web-src/src/components/ListItemGenre.vue
Normal file
99
web-src/src/components/ListItemGenre.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="media">
|
||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_genre">
|
||||
<h1 class="title is-6">{{ genre.name }}</h1>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
<a @click="show_details_modal = true">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
<modal-dialog :show="show_details_modal" @close="show_details_modal = false">
|
||||
<template slot="modal-content">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="title is-4">
|
||||
<a class="has-text-link" @click="open_genre">{{ genre.name }}</a>
|
||||
</p>
|
||||
<!--
|
||||
<div class="content is-small">
|
||||
<p>
|
||||
<span class="heading">Albums</span>
|
||||
<span class="title is-6">{{ genre.album_count }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Tracks</span>
|
||||
<span class="title is-6">{{ genre.track_count }}</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 mdi-12px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-12px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-12px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalDialog from '@/components/ModalDialog'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'PartGenre',
|
||||
components: { ModalDialog },
|
||||
|
||||
props: [ 'genre' ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
show_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.library_genre(this.genre.name).then(({ data }) =>
|
||||
webapi.player_play_uri(data.albums.items.map(a => a.uri).join(','), false)
|
||||
)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.library_genre(this.genre.name).then(({ data }) =>
|
||||
webapi.queue_add(data.albums.items.map(a => a.uri).join(',')).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Genre albums appended to queue', type: 'info', timeout: 1500 })
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.library_genre(this.genre.name).then(({ data }) =>
|
||||
webapi.queue_add_next(data.albums.items.map(a => a.uri).join(',')).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Genre albums playing next', type: 'info', timeout: 1500 })
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
open_genre: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ name: 'Genre', params: { genre: this.genre.name } })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -25,6 +25,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -55,11 +58,7 @@ export default {
|
||||
methods: {
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.playlist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.playlist.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -69,6 +68,13 @@ export default {
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_add_next(this.playlist.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
},
|
||||
|
||||
open_playlist: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/playlists/' + this.playlist.id })
|
||||
|
@ -107,7 +107,7 @@ export default {
|
||||
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.player_playid(this.item.id)
|
||||
webapi.player_play({ 'item_id': this.item.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play_track">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -96,20 +99,12 @@ export default {
|
||||
methods: {
|
||||
play: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.context_uri).then(() =>
|
||||
webapi.player_playpos(this.position)
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.context_uri, false, this.position)
|
||||
},
|
||||
|
||||
play_track: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.track.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.track.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -119,6 +114,13 @@ export default {
|
||||
)
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
this.show_details_modal = false
|
||||
webapi.queue_add_next(this.track.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
},
|
||||
|
||||
open_album: function () {
|
||||
this.show_details_modal = false
|
||||
if (this.track.media_kind === 'podcast') {
|
||||
|
@ -36,6 +36,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -65,12 +68,8 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.album.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -81,6 +80,13 @@ export default {
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
webapi.queue_add_next(this.album.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Album tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
show_details: function () {
|
||||
this.show_details_modal = true
|
||||
},
|
||||
|
@ -31,6 +31,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -60,12 +63,8 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.artist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.artist.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -75,6 +74,13 @@ export default {
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
webapi.queue_add_next(this.artist.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Artist tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
show_details: function () {
|
||||
this.show_details_modal = true
|
||||
},
|
||||
|
@ -36,6 +36,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -65,12 +68,8 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.playlist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.playlist.uri, false)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -80,6 +79,13 @@ export default {
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
webapi.queue_add_next(this.playlist.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Playlist tracks appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
show_details: function () {
|
||||
this.show_details_modal = true
|
||||
},
|
||||
@ -90,7 +96,7 @@ export default {
|
||||
|
||||
open_playlist: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.owner.id + '/' + this.playlist.id })
|
||||
this.$router.push({ path: '/music/spotify/playlists/' + this.playlist.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,9 @@
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add">
|
||||
<span class="icon"><i class="mdi mdi-playlist-plus mdi-18px"></i></span> <span>Add</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="queue_add_next">
|
||||
<span class="icon"><i class="mdi mdi-playlist-play mdi-18px"></i></span> <span>Add Next</span>
|
||||
</a>
|
||||
<a class="card-footer-item has-text-dark" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play mdi-18px"></i></span> <span>Play</span>
|
||||
</a>
|
||||
@ -80,12 +83,8 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.context_uri).then(() =>
|
||||
webapi.player_playpos(this.position)
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.context_uri, false, this.position)
|
||||
},
|
||||
|
||||
queue_add: function () {
|
||||
@ -95,6 +94,13 @@ export default {
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
queue_add_next: function () {
|
||||
webapi.queue_add_next(this.track.uri).then(() =>
|
||||
this.$store.dispatch('add_notification', { text: 'Track appended to queue', type: 'info', timeout: 2000 })
|
||||
)
|
||||
this.show_details_modal = false
|
||||
},
|
||||
|
||||
show_details: function () {
|
||||
this.show_details_modal = true
|
||||
},
|
||||
|
@ -23,6 +23,12 @@
|
||||
<span class="">Albums</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" to="/music/genres" active-class="is-active">
|
||||
<a>
|
||||
<span class="icon is-small"><i class="mdi mdi-speaker"></i></span>
|
||||
<span class="">Genres</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" to="/music/spotify" v-if="spotify_enabled" active-class="is-active">
|
||||
<a>
|
||||
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span>
|
||||
|
@ -30,12 +30,40 @@ a.navbar-item {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.fd-has-margin-top {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.fd-has-margin-bottom {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.fd-has-padding-left-right {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.fd-is-text-clipped {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fd-is-fullheight {
|
||||
height: calc(100vh - 3.25rem - 3.25rem);
|
||||
}
|
||||
|
||||
.fd-is-fullheight-body {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.fd-image-fullheight {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.fd-tabs-section {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@ -44,6 +72,10 @@ a.navbar-item {
|
||||
top: 52px !important;
|
||||
}
|
||||
|
||||
.fd-has-shadow {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
.sortable-chosen .media-right {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@
|
||||
<div class="column is-four-fifths">
|
||||
<div class="content has-text-centered-mobile">
|
||||
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p>
|
||||
<p class="is-size-7"><a href="https://github.com/chme/forked-daapd-web">Web interface</a> v{{ version }} built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/chme/forked-daapd-web/network/dependencies">more</a>.</p>
|
||||
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="https://materialdesignicons.com/">Material Design Icons</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a> and <a href="https://github.com/ejurgensen/forked-daapd/network/dependencies">more</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,12 +83,6 @@ import webapi from '@/webapi'
|
||||
export default {
|
||||
name: 'PageAbout',
|
||||
|
||||
data () {
|
||||
return {
|
||||
'version': process.env.V2
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
config () {
|
||||
return this.$store.state.config
|
||||
|
@ -5,12 +5,16 @@
|
||||
<a class="title is-4 has-text-link has-text-weight-normal" @click="open_artist">{{ album.artist }}</a>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-play"></i>
|
||||
</span>
|
||||
<span>Play</span>
|
||||
</a>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
<!--
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Play</span>
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
|
||||
@ -58,11 +62,7 @@ export default {
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.album.uri, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,13 @@
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ artist.name }}</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | {{ artist.track_count }} tracks</p>
|
||||
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
|
||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album"></list-item-album>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
@ -43,6 +48,13 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_tracks: function () {
|
||||
this.$router.push({ path: '/music/artists/' + this.artist.id + '/tracks' })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -53,11 +53,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.album.uri, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
web-src/src/pages/PageGenre.vue
Normal file
76
web-src/src/pages/PageGenre.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ name }}</p>
|
||||
<p class="heading">{{ genreAlbums.total }} albums</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-albums v-for="album in genreAlbums.items" :key="album.id" :album="album" :links="links"></list-item-albums>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemAlbums from '@/components/ListItemAlbum'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const genreData = {
|
||||
load: function (to) {
|
||||
return webapi.library_genre(to.params.genre)
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.name = vm.$route.params.genre
|
||||
vm.genreAlbums = response.data.albums
|
||||
var li = 0
|
||||
var v = null
|
||||
var i
|
||||
for (i = 0; i < vm.genreAlbums.items.length; i++) {
|
||||
var n = vm.genreAlbums.items[i].name_sort.charAt(0).toUpperCase()
|
||||
if (n !== v) {
|
||||
var obj = {}
|
||||
obj.n = n
|
||||
obj.a = 'idx_nav_' + li
|
||||
vm.links.push(obj)
|
||||
li++
|
||||
v = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageGenre',
|
||||
mixins: [ LoadDataBeforeEnterMixin(genreData) ],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbums },
|
||||
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
genreAlbums: {},
|
||||
links: []
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.player_play_uri(this.genreAlbums.items.map(a => a.uri).join(','), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
51
web-src/src/pages/PageGenres.vue
Normal file
51
web-src/src/pages/PageGenres.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">Genres</p>
|
||||
<p class="heading">{{ genres.total }} genres</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre"></list-item-genre>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemGenre from '@/components/ListItemGenre'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const genresData = {
|
||||
load: function (to) {
|
||||
return webapi.library_genres()
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.genres = response.data
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageGenres',
|
||||
mixins: [ LoadDataBeforeEnterMixin(genresData) ],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemGenre },
|
||||
|
||||
data () {
|
||||
return {
|
||||
genres: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<p class="heading">NOW PLAYING</p>
|
||||
<h1 class="title is-3">
|
||||
<section class="hero fd-is-fullheight">
|
||||
<div class="hero-head fd-has-padding-left-right">
|
||||
<div class="container has-text-centered fd-has-margin-top">
|
||||
<h1 class="title is-4">
|
||||
{{ now_playing.title }}
|
||||
</h1>
|
||||
<h2 class="title is-5">
|
||||
<h2 class="title is-6">
|
||||
{{ now_playing.artist }}
|
||||
</h2>
|
||||
<h3 class="subtitle is-5">
|
||||
<h3 class="subtitle is-6">
|
||||
{{ now_playing.album }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-body fd-is-fullheight-body has-text-centered" v-show="artwork_visible">
|
||||
<img :src="artwork_url" class="fd-has-shadow fd-image-fullheight" @load="artwork_loaded" @error="artwork_error">
|
||||
</div>
|
||||
<div class="hero-foot fd-has-padding-left-right">
|
||||
<div class="container has-text-centered fd-has-margin-bottom">
|
||||
<p class="control has-text-centered fd-progress-now-playing">
|
||||
<range-slider
|
||||
class="seek-slider fd-has-action"
|
||||
@ -26,14 +32,14 @@
|
||||
<p class="content">
|
||||
<span>{{ item_progress_ms | duration }} / {{ now_playing.length_ms | duration }}</span>
|
||||
</p>
|
||||
<p class="control has-text-centered">
|
||||
<div class="buttons has-addons is-centered">
|
||||
<player-button-previous class="button is-medium"></player-button-previous>
|
||||
<player-button-play-pause class="button is-medium" icon_style="mdi-36px"></player-button-play-pause>
|
||||
<player-button-next class="button is-medium"></player-button-next>
|
||||
<player-button-repeat class="button is-medium is-light"></player-button-repeat>
|
||||
<player-button-shuffle class="button is-medium is-light"></player-button-shuffle>
|
||||
<player-button-consume class="button is-medium is-light"></player-button-consume>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -57,7 +63,8 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
item_progress_ms: 0,
|
||||
interval_id: 0
|
||||
interval_id: 0,
|
||||
artwork_visible: false
|
||||
}
|
||||
},
|
||||
|
||||
@ -84,6 +91,13 @@ export default {
|
||||
},
|
||||
now_playing () {
|
||||
return this.$store.getters.now_playing
|
||||
},
|
||||
|
||||
artwork_url: function () {
|
||||
if (this.now_playing.artwork_url && this.now_playing.artwork_url.startsWith('/')) {
|
||||
return this.now_playing.artwork_url + '?maxwidth=600&maxheight=600'
|
||||
}
|
||||
return this.now_playing.artwork_url
|
||||
}
|
||||
},
|
||||
|
||||
@ -96,6 +110,14 @@ export default {
|
||||
webapi.player_seek(newPosition).catch(() => {
|
||||
this.item_progress_ms = this.state.item_progress_ms
|
||||
})
|
||||
},
|
||||
|
||||
artwork_loaded: function () {
|
||||
this.artwork_visible = true
|
||||
},
|
||||
|
||||
artwork_error: function () {
|
||||
this.artwork_visible = false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -5,10 +5,7 @@
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-play"></i>
|
||||
</span>
|
||||
<span>Play</span>
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
@ -52,11 +49,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.playlist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.playlist.uri, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,11 +52,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
webapi.player_play_uri(this.album.uri, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
web-src/src/pages/PageTracks.vue
Normal file
66
web-src/src/pages/PageTracks.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ artist.name }}</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p>
|
||||
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" :position="index" :context_uri="track.uri"></list-item-track>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const tracksData = {
|
||||
load: function (to) {
|
||||
return Promise.all([
|
||||
webapi.library_artist(to.params.artist_id),
|
||||
webapi.library_artist_tracks(to.params.artist_id)
|
||||
])
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.artist = response[0].data
|
||||
vm.tracks = response[1].data.tracks
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageTracks',
|
||||
mixins: [ LoadDataBeforeEnterMixin(tracksData) ],
|
||||
components: { ContentWithHeading, ListItemTrack },
|
||||
|
||||
data () {
|
||||
return {
|
||||
artist: {},
|
||||
tracks: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_artist: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/music/artists/' + this.artist.id })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -6,10 +6,7 @@
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-play"></i>
|
||||
</span>
|
||||
<span>Play</span>
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
@ -56,12 +53,8 @@ export default {
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.album.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.album.uri, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,7 @@
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon">
|
||||
<i class="mdi mdi-play"></i>
|
||||
</span>
|
||||
<span>Play</span>
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="content">
|
||||
@ -33,8 +30,8 @@ const playlistData = {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(store.state.spotify.webapi_token)
|
||||
return Promise.all([
|
||||
spotifyApi.getPlaylist(to.params.user_id, to.params.playlist_id),
|
||||
spotifyApi.getPlaylistTracks(to.params.user_id, to.params.playlist_id, { limit: 50, offset: 0 })
|
||||
spotifyApi.getPlaylist(to.params.playlist_id),
|
||||
spotifyApi.getPlaylistTracks(to.params.playlist_id, { limit: 50, offset: 0 })
|
||||
])
|
||||
},
|
||||
|
||||
@ -65,7 +62,7 @@ export default {
|
||||
load_next: function ($state) {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi.getPlaylistTracks(this.playlist.owner.id, this.playlist.id, { limit: 50, offset: this.offset }).then(data => {
|
||||
spotifyApi.getPlaylistTracks(this.playlist.id, { limit: 50, offset: this.offset }).then(data => {
|
||||
this.append_tracks(data, $state)
|
||||
})
|
||||
},
|
||||
@ -84,12 +81,8 @@ export default {
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.queue_clear().then(() =>
|
||||
webapi.queue_add(this.playlist.uri).then(() =>
|
||||
webapi.player_play()
|
||||
)
|
||||
)
|
||||
this.show_details_modal = false
|
||||
webapi.player_play_uri(this.playlist.uri, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ import PageArtists from '@/pages/PageArtists'
|
||||
import PageArtist from '@/pages/PageArtist'
|
||||
import PageAlbums from '@/pages/PageAlbums'
|
||||
import PageAlbum from '@/pages/PageAlbum'
|
||||
import PageGenres from '@/pages/PageGenres'
|
||||
import PageGenre from '@/pages/PageGenre'
|
||||
import PageTracks from '@/pages/PageTracks'
|
||||
import PagePodcasts from '@/pages/PagePodcasts'
|
||||
import PagePodcast from '@/pages/PagePodcast'
|
||||
import PageAudiobooks from '@/pages/PageAudiobooks'
|
||||
@ -80,6 +83,12 @@ export const router = new VueRouter({
|
||||
component: PageArtist,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/music/artists/:artist_id/tracks',
|
||||
name: 'Tracks',
|
||||
component: PageTracks,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/music/albums',
|
||||
name: 'Albums',
|
||||
@ -92,6 +101,18 @@ export const router = new VueRouter({
|
||||
component: PageAlbum,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/music/genres',
|
||||
name: 'Genres',
|
||||
component: PageGenres,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/music/genres/:genre',
|
||||
name: 'Genre',
|
||||
component: PageGenre,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/podcasts',
|
||||
name: 'Podcasts',
|
||||
@ -168,7 +189,7 @@ export const router = new VueRouter({
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/music/spotify/playlists/:user_id/:playlist_id',
|
||||
path: '/music/spotify/playlists/:playlist_id',
|
||||
name: 'Spotify Playlist',
|
||||
component: SpotifyPagePlaylist,
|
||||
meta: { show_progress: true }
|
||||
|
@ -45,12 +45,30 @@ export default {
|
||||
return axios.post('/api/queue/items/add?uris=' + uri)
|
||||
},
|
||||
|
||||
queue_add_next (uri) {
|
||||
var position = 0
|
||||
if (store.getters.now_playing && store.getters.now_playing.id) {
|
||||
position = store.getters.now_playing.position + 1
|
||||
}
|
||||
return axios.post('/api/queue/items/add?uris=' + uri + '&position=' + position)
|
||||
},
|
||||
|
||||
player_status () {
|
||||
return axios.get('/api/player')
|
||||
},
|
||||
|
||||
player_play () {
|
||||
return axios.put('/api/player/play')
|
||||
player_play_uri (uris, shuffle, position = 0) {
|
||||
return this.queue_clear().then(() =>
|
||||
this.player_shuffle(shuffle).then(() =>
|
||||
this.queue_add(uris).then(() =>
|
||||
this.player_play({ 'position': position })
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
player_play (options = {}) {
|
||||
return axios.put('/api/player/play', undefined, { params: options })
|
||||
},
|
||||
|
||||
player_playpos (position) {
|
||||
@ -130,6 +148,33 @@ export default {
|
||||
return axios.get('/api/library/albums/' + albumId + '/tracks')
|
||||
},
|
||||
|
||||
library_genres () {
|
||||
return axios.get('/api/library/genres')
|
||||
},
|
||||
|
||||
library_genre (genre) {
|
||||
var genreParams = {
|
||||
'type': 'albums',
|
||||
'media_kind': 'music',
|
||||
'expression': 'genre is "' + genre + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: genreParams
|
||||
})
|
||||
},
|
||||
|
||||
library_artist_tracks (artist) {
|
||||
if (artist) {
|
||||
var artistParams = {
|
||||
'type': 'tracks',
|
||||
'expression': 'songartistid is "' + artist + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
params: artistParams
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
library_podcasts () {
|
||||
return axios.get('/api/library/albums?media_kind=podcast')
|
||||
},
|
||||
|
34
web-src/vue.config.js
Normal file
34
web-src/vue.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
// Runtime compiler is required to compile vue templates
|
||||
runtimeCompiler: true,
|
||||
|
||||
// Output path for the generated static assets (js/css)
|
||||
outputDir: '../htdocs',
|
||||
|
||||
// Output path for the generated index.html
|
||||
indexPath: 'index.html',
|
||||
|
||||
assetsDir: 'player',
|
||||
|
||||
// Do not add hashes to the generated js/css filenames, would otherwise
|
||||
// require to adjust the Makefile in htdocs each time the web interface is
|
||||
// build
|
||||
filenameHashing: false,
|
||||
|
||||
css: {
|
||||
sourceMap: true
|
||||
},
|
||||
|
||||
devServer: {
|
||||
// Proxy forked-daapd JSON API calls to the forked-daapd server running on
|
||||
// localhost:3689
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3689',
|
||||
},
|
||||
'/artwork': {
|
||||
target: 'http://localhost:3689',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user