Merge pull request #617 from chme/webinterface

Update player web interface
This commit is contained in:
chme 2018-11-23 14:31:24 +01:00 committed by GitHub
commit 0d8a764f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 9163 additions and 14762 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -82,6 +82,9 @@ STRTAG : 'artist'
| 'path'
| 'type'
| 'grouping'
| 'artist_id'
| 'album_id'
| 'songartistid'
;
INTTAG : 'play_count'

View File

@ -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
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

View File

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

View File

@ -1,4 +0,0 @@
/build/
/config/
/dist/
/*.js

View File

@ -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'
}
}

View File

@ -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
View File

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

View File

@ -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'
))
})
})

View File

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

View File

@ -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')
})
}
}

View File

@ -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'
}
}

View File

@ -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'
}
}

View File

@ -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)
}
})
})

View File

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

View File

@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -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
}
}

View File

@ -1,6 +0,0 @@
'use strict'
module.exports = {
NODE_ENV: '"production"',
VERSION: '"123"',
V2: JSON.stringify(require("../package.json").version)
}

18601
web-src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

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

View File

@ -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">

View File

@ -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)

View File

@ -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') {

View File

@ -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 })

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

View File

@ -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 })

View File

@ -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 })
}
}
}

View File

@ -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') {

View File

@ -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
},

View File

@ -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
},

View File

@ -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 })
}
}
}

View File

@ -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
},

View File

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

View File

@ -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;
}

View File

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

View File

@ -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">
<div class="buttons is-centered">
<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>
<!--
<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)
}
}
}

View File

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

View File

@ -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)
}
}
}

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

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

View File

@ -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
}
},

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

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

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 }

View File

@ -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
View 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',
}
}
}
}