diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..b73094a5fe --- /dev/null +++ b/.babelrc @@ -0,0 +1,22 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "corejs": "3", + "useBuiltIns": "usage" + } + ], + [ + "minify", + { + "builtIns": false, + "evaluate": false, + "mangle": false + } + ] + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..0f1786729b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..3eed71dc66 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,24 @@ +{ + "extends": "standard", + "plugins": [ + "html" + ], + "parser": "babel-eslint", + "rules": { + "no-new": "off", + "camelcase": "off", + "no-return-assign": "off", + "space-before-function-paren": ["error", "never"], + "no-var": "error", + "no-fallthrough": "off", + "prefer-promise-reject-errors": "off", + "eqeqeq": "off", + "no-multiple-empty-lines": [1, {"max": 2}], + "comma-dangle": [2, "always-multiline"], + "standard/no-callback-literal": "off", + "prefer-const": "off" + }, + "settings": { + "html/html-extensions": [".html", ".vue"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..879b4c1dba --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + + +build + +dist + +publish/assets + +publish/utils/githubToken.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..2777672d5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# lx-music-desktop change log + +All notable changes to this project will be documented in this file. + +Project versioning adheres to [Semantic Versioning](http://semver.org/). +Commit convention is based on [Conventional Commits](http://conventionalcommits.org). +Change log format is based on [Keep a Changelog](http://keepachangelog.com/). + +## [0.1.0] - 2019-8-16 + +* 0.1.0版本发布 diff --git a/README.md b/README.md index ed4a4c37a1..4d8264390b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -# lx-music-desktop -洛雪音乐助手桌面版 +# 洛雪音乐助手桌面版 + +[![GitHub release][1]][2] +[![Build status][3]][4] +[![GitHub All Releases Download][5]][6] +[![dev branch][7]][8] + +[1]: https://img.shields.io/github/release/lyswhut/lx-music-desktop +[2]: https://github.com/lyswhut/lx-music-desktop/releases +[3]: https://ci.appveyor.com/api/projects/status/flrsqd5ymp8fnte5?svg=true +[4]: https://ci.appveyor.com/project/lyswhut/lx-music-desktop +[5]: https://img.shields.io/github/downloads/lyswhut/lx-music-desktop/total +[6]: https://github.com/lyswhut/lx-music-desktop/releases +[7]: https://img.shields.io/github/package-json/v/lyswhut/lx-music-desktop/dev +[8]: https://github.com/lyswhut/lx-music-desktop/tree/dev + +## 说明 + +一个基于 Electron + Vue 开发的 Windows 版音乐软件。 + +所用技术栈: + +- Electron 7.x +- Vue 2.x + +其他说明:TODO + +感谢 提供的部分音乐API! + +## 使用方法 + +```bash +# 开发模式 +npm run dev + +# 构建免安装版 +npm run pack:dir + +# 构建安装包 +npm run pack + +``` diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..1ac2e2f341 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,20 @@ +platform: + - x64 + +cache: + - node_modules + - '%APPDATA%\npm-cache' + # - '%USERPROFILE%\.electron' + +install: + - ps: Install-Product node 12 x64 + - npm install + +build_script: + - npm run pub:gh + +test: off + +branches: + only: + - master diff --git a/build-config/css-loader.config.js b/build-config/css-loader.config.js new file mode 100644 index 0000000000..b6cb4288d8 --- /dev/null +++ b/build-config/css-loader.config.js @@ -0,0 +1,8 @@ +const isDev = process.env.NODE_ENV === 'development' + +module.exports = { + modules: { + localIdentName: isDev ? '[folder]-[name]--[local]--[hash:base64:5]' : '[hash:base64:5]', + }, + localsConvention: 'camelCase', +} diff --git a/build-config/main/webpack.config.base.js b/build-config/main/webpack.config.base.js new file mode 100644 index 0000000000..eb5e67a969 --- /dev/null +++ b/build-config/main/webpack.config.base.js @@ -0,0 +1,44 @@ +const path = require('path') + +module.exports = { + target: 'electron-main', + output: { + filename: '[name].js', + libraryTarget: 'commonjs2', + path: path.join(__dirname, '../../dist/electron'), + }, + externals: [ + // suppress electron-debug warning + // see https://github.com/SimulatedGREG/electron-vue/issues/498 + { 'electron-debug': 'electron-debug' }, + ], + resolve: { + alias: { + common: path.join(__dirname, '../../src/common'), + }, + extensions: ['*', '.js', '.json', '.node'], + }, + module: { + rules: [ + { + test: /\.js$/, + use: { + loader: 'eslint-loader', + options: { + formatter: require('eslint-formatter-friendly'), + }, + }, + exclude: /node_modules/, + enforce: 'pre', + }, + // { + // test: /\.js$/, + // loader: 'babel-loader', + // exclude: /node_modules/, + // }, + ], + }, + performance: { + maxEntrypointSize: 300000, + }, +} diff --git a/build-config/main/webpack.config.dev.js b/build-config/main/webpack.config.dev.js new file mode 100644 index 0000000000..e77412589a --- /dev/null +++ b/build-config/main/webpack.config.dev.js @@ -0,0 +1,22 @@ +const path = require('path') +const merge = require('webpack-merge') +const webpack = require('webpack') + +const baseConfig = require('./webpack.config.base') + + +module.exports = merge(baseConfig, { + mode: 'development', + entry: { + main: path.join(__dirname, '../../src/main/index.dev.js'), + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"development"', + }, + __static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`, + }), + new webpack.NoEmitOnErrorsPlugin(), + ], +}) diff --git a/build-config/main/webpack.config.prod.js b/build-config/main/webpack.config.prod.js new file mode 100644 index 0000000000..53499096cf --- /dev/null +++ b/build-config/main/webpack.config.prod.js @@ -0,0 +1,29 @@ +const path = require('path') +const merge = require('webpack-merge') +const webpack = require('webpack') + +const baseConfig = require('./webpack.config.base') + +const { dependencies } = require('../../package.json') + + +module.exports = merge(baseConfig, { + mode: 'production', + entry: { + main: path.join(__dirname, '../../src/main/index.js'), + }, + externals: [ + ...Object.keys(dependencies || {}), + ], + node: { + __dirname: false, + __filename: false, + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"production"', + }, + }), + ], +}) diff --git a/build-config/pack.js b/build-config/pack.js new file mode 100644 index 0000000000..e311759992 --- /dev/null +++ b/build-config/pack.js @@ -0,0 +1,84 @@ +process.env.NODE_ENV = 'production' + +const chalk = require('chalk') +const del = require('del') +const webpack = require('webpack') +const Multispinner = require('multispinner') + +const mainConfig = require('./main/webpack.config.prod') +const rendererConfig = require('./renderer/webpack.config.prod') + +const errorLog = chalk.bgRed.white(' ERROR ') + ' ' +const okayLog = chalk.bgGreen.white(' OKAY ') + ' ' + + +function build() { + del.sync(['dist/electron', 'build']) + + const tasks = ['main', 'renderer'] + const m = new Multispinner(tasks, { + preText: 'building', + postText: 'process', + }) + + let results = '' + + m.on('success', () => { + process.stdout.write('\x1B[2J\x1B[0f') + console.log(`\n\n${results}`) + console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) + process.exit() + }) + + pack(mainConfig).then(result => { + results += result + '\n\n' + m.success('main') + }).catch(err => { + m.error('main') + console.log(`\n ${errorLog}failed to build main process`) + console.error(`\n${err}\n`) + process.exit(1) + }) + + pack(rendererConfig).then(result => { + results += result + '\n\n' + m.success('renderer') + }).catch(err => { + m.error('renderer') + console.log(`\n ${errorLog}failed to build renderer process`) + console.error(`\n${err}\n`) + process.exit(1) + }) +} + +function pack(config) { + return new Promise((resolve, reject) => { + config.mode = 'production' + webpack(config, (err, stats) => { + if (err) reject(err.stack || err) + else if (stats.hasErrors()) { + let err = '' + + stats.toString({ + chunks: false, + modules: false, + colors: true, + }) + .split(/\r?\n/) + .forEach(line => { + err += ` ${line}\n` + }) + + reject(err) + } else { + resolve(stats.toString({ + chunks: false, + colors: true, + })) + } + }) + }) +} + +build() + diff --git a/build-config/renderer/webpack.config.base.js b/build-config/renderer/webpack.config.base.js new file mode 100644 index 0000000000..b1c98849e2 --- /dev/null +++ b/build-config/renderer/webpack.config.base.js @@ -0,0 +1,102 @@ +const path = require('path') +const VueLoaderPlugin = require('vue-loader/lib/plugin') +const HTMLPlugin = require('html-webpack-plugin') + +const vueLoaderConfig = require('../vue-loader.config') + + +module.exports = { + target: 'electron-renderer', + entry: { + renderer: path.join(__dirname, '../../src/renderer/main.js'), + }, + output: { + filename: '[name].js', + libraryTarget: 'commonjs2', + path: path.join(__dirname, '../../dist/electron'), + publicPath: './', + }, + resolve: { + alias: { + '@': path.join(__dirname, '../../src/renderer'), + common: path.join(__dirname, '../../src/common'), + }, + extensions: ['*', '.js', '.json', '.vue', '.node'], + }, + module: { + rules: [ + { + test: /\.(vue|js)$/, + use: { + loader: 'eslint-loader', + options: { + formatter: require('eslint-formatter-friendly'), + }, + }, + exclude: /node_modules/, + enforce: 'pre', + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig, + }, + { + test: /\.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.pug$/, + oneOf: [ + // Use pug-plain-loader handle .vue file + { + resourceQuery: /vue/, + use: ['pug-plain-loader'], + }, + // Use pug-loader handle .pug file + { + use: ['pug-loader'], + }, + ], + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: 'imgs/[name]--[folder].[ext]', + }, + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: 'media/[name]--[folder].[ext]', + }, + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: 'fonts/[name]--[folder].[ext]', + }, + }, + ], + }, + performance: { + maxEntrypointSize: 300000, + }, + plugins: [ + new HTMLPlugin({ + filename: 'index.html', + template: path.join(__dirname, '../../src/index.pug'), + isProd: process.env.NODE_ENV == 'production', + browser: process.browser, + __dirname, + }), + new VueLoaderPlugin(), + ], +} diff --git a/build-config/renderer/webpack.config.dev.js b/build-config/renderer/webpack.config.dev.js new file mode 100644 index 0000000000..2bce67f3dc --- /dev/null +++ b/build-config/renderer/webpack.config.dev.js @@ -0,0 +1,56 @@ +const path = require('path') +const webpack = require('webpack') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') + +const merge = require('webpack-merge') + +const baseConfig = require('./webpack.config.base') + +const { mergeCSSLoaderDev } = require('../utils') + +module.exports = merge(baseConfig, { + mode: 'development', + devtool: '#cheap-module-eval-source-map', + module: { + rules: [ + { + test: /\.css$/, + oneOf: mergeCSSLoaderDev(), + }, + { + test: /\.less$/, + oneOf: mergeCSSLoaderDev({ + loader: 'less-loader', + options: { + sourceMap: true, + }, + }), + }, + { + test: /\.styl(:?us)?$/, + oneOf: mergeCSSLoaderDev({ + loader: 'stylus-loader', + options: { + sourceMap: true, + }, + }), + }, + ], + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new webpack.NoEmitOnErrorsPlugin(), + new FriendlyErrorsPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"development"', + ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', + }, + '__static': `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`, + }), + ], + performance: { + hints: false, + }, +}) + diff --git a/build-config/renderer/webpack.config.prod.js b/build-config/renderer/webpack.config.prod.js new file mode 100644 index 0000000000..df861235f2 --- /dev/null +++ b/build-config/renderer/webpack.config.prod.js @@ -0,0 +1,86 @@ +const path = require('path') +const webpack = require('webpack') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') +const TerserPlugin = require('terser-webpack-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const merge = require('webpack-merge') + +const baseConfig = require('./webpack.config.base') + +const { mergeCSSLoaderProd } = require('../utils') +const { dependencies } = require('../../package.json') + +let whiteListedModules = ['vue'] + + +module.exports = merge(baseConfig, { + mode: 'production', + devtool: false, + externals: [ + ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)), + ], + module: { + rules: [ + { + test: /\.css$/, + oneOf: mergeCSSLoaderProd(), + }, + { + test: /\.less$/, + oneOf: mergeCSSLoaderProd({ + loader: 'less-loader', + options: { + sourceMap: true, + }, + }), + }, + { + test: /\.styl(:?us)?$/, + oneOf: mergeCSSLoaderProd({ + loader: 'stylus-loader', + options: { + sourceMap: true, + }, + }), + }, + ], + }, + plugins: [ + new CopyWebpackPlugin([ + { + from: path.join(__dirname, '../../src/static'), + to: path.join(__dirname, '../../dist/electron/static'), + ignore: ['.*'], + }, + ]), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"production"', + }, + }), + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + new webpack.NamedChunksPlugin(), + ], + optimization: { + minimizer: [ + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: false, // set to true if you want JS source maps + }), + new OptimizeCSSAssetsPlugin({}), + ], + }, + performance: { + hints: 'warning', + }, + node: { + __dirname: false, + __filename: false, + }, +}) + + diff --git a/build-config/runner-dev.js b/build-config/runner-dev.js new file mode 100644 index 0000000000..d2cccf0d59 --- /dev/null +++ b/build-config/runner-dev.js @@ -0,0 +1,170 @@ +process.env.NODE_ENV = 'development' + +const chalk = require('chalk') +const electron = require('electron') +const path = require('path') +// const { say } = require('cfonts') +const { spawn } = require('child_process') +const webpack = require('webpack') +const WebpackDevServer = require('webpack-dev-server') +const webpackHotMiddleware = require('webpack-hot-middleware') + +const mainConfig = require('./main/webpack.config.dev') +const rendererConfig = require('./renderer/webpack.config.dev') + +const { logStats } = require('./utils') + +let electronProcess = null +let manualRestart = false +let hotMiddleware + + +function startRenderer() { + return new Promise((resolve, reject) => { + // rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) + // rendererConfig.mode = 'development' + const compiler = webpack(rendererConfig) + hotMiddleware = webpackHotMiddleware(compiler, { + log: false, + heartbeat: 2500, + }) + + compiler.hooks.compilation.tap('compilation', compilation => { + // console.log(Object.keys(compilation.hooks)) + compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { + hotMiddleware.publish({ action: 'reload' }) + cb() + }) + }) + + compiler.hooks.done.tap('done', stats => { + // logStats('Renderer', 'Compile done') + // logStats('Renderer', stats) + }) + + const server = new WebpackDevServer( + compiler, + { + contentBase: path.join(__dirname, '../'), + quiet: true, + hot: true, + historyApiFallback: true, + clientLogLevel: 'warning', + overlay: { + errors: true, + }, + before(app, ctx) { + app.use(hotMiddleware) + ctx.middleware.waitUntilValid(() => { + resolve() + }) + }, + } + ) + + server.listen(9080) + }) +} + +function startMain() { + return new Promise((resolve, reject) => { + // mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) + // mainConfig.mode = 'development' + const compiler = webpack(mainConfig) + + compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { + logStats('Main', chalk.white.bold('compiling...')) + hotMiddleware.publish({ action: 'compiling' }) + done() + }) + + compiler.watch({}, (err, stats) => { + if (err) { + console.log(err) + return + } + + // logStats('Main', stats) + + if (electronProcess && electronProcess.kill) { + manualRestart = true + process.kill(electronProcess.pid) + electronProcess = null + startElectron() + + setTimeout(() => { + manualRestart = false + }, 5000) + } + + resolve() + }) + }) +} + +function startElectron() { + let args = [ + '--inspect=5858', + // 'NODE_ENV=development', + path.join(__dirname, '../dist/electron/main.js'), + ] + + // detect yarn or npm and process commandline args accordingly + if (process.env.npm_execpath.endsWith('yarn.js')) { + args = args.concat(process.argv.slice(3)) + } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { + args = args.concat(process.argv.slice(2)) + } + + electronProcess = spawn(electron, args) + + electronProcess.stdout.on('data', data => { + electronLog(data, 'blue') + }) + electronProcess.stderr.on('data', data => { + electronLog(data, 'red') + }) + + electronProcess.on('close', () => { + if (!manualRestart) process.exit() + }) +} + +function electronLog(data, color) { + let log = data.toString() + if (/[0-9A-z]+/.test(log)) { + console.log(chalk[color](log)) + } +} + +function greeting() { +/* const cols = process.stdout.columns + let text = '' + + if (cols > 104) text = 'electron-vue' + else if (cols > 76) text = 'electron-|vue' + else text = false + + if (text) { + say(text, { + colors: ['yellow'], + font: 'simple3d', + space: false, + }) + } else console.log(chalk.yellow.bold('\n electron-vue')) */ + console.log(chalk.blue('getting ready...') + '\n') +} + +function init() { + greeting() + + Promise.all([startRenderer(), startMain()]) + .then(() => { + startElectron() + }) + .catch(err => { + console.error(err) + }) +} + +init() diff --git a/build-config/utils.js b/build-config/utils.js new file mode 100644 index 0000000000..0a6a9e7261 --- /dev/null +++ b/build-config/utils.js @@ -0,0 +1,103 @@ +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const cssLoaderConfig = require('./css-loader.config') +const chalk = require('chalk') + +// merge css-loader in development +exports.mergeCSSLoaderDev = beforeLoader => { + const loader = [ + // 这里匹配 ` + diff --git a/src/renderer/assets/styles/animate.less b/src/renderer/assets/styles/animate.less new file mode 100644 index 0000000000..41ade4af09 --- /dev/null +++ b/src/renderer/assets/styles/animate.less @@ -0,0 +1,689 @@ + +@keyframes flipInX { + from { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutX { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +@keyframes flipInY { + from { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutY { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} +@keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + transform: scale3d(1, 1, 1); + } +} +@keyframes bounceOut { + 20% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 50%, + 55% { + opacity: 1; + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } +} +@keyframes lightSpeedIn { + from { + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + transform: skewX(20deg); + opacity: 1; + } + + 80% { + transform: skewX(-5deg); + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +@keyframes rotateIn { + from { + transform-origin: center; + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + transform-origin: center; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateOut { + from { + transform-origin: center; + opacity: 1; + } + + to { + transform-origin: center; + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +@keyframes rotateOutDownLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +@keyframes rotateOutDownRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +@keyframes rollIn { + from { + opacity: 0; + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} +@keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +@keyframes zoomIn { + from { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 50% { + opacity: 1; + } +} +@keyframes zoomInDown { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInLeft { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInRight { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInUp { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + to { + opacity: 0; + } +} +@keyframes zoomOutDown { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutLeft { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(-2000px, 0, 0); + transform-origin: left center; + } +} +@keyframes zoomOutRight { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(2000px, 0, 0); + transform-origin: right center; + } +} +@keyframes zoomOutUp { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes slideInDown { + from { + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInLeft { + from { + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInRight { + from { + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInUp { + from { + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideOutDown { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, 100%, 0); + } +} +@keyframes slideOutLeft { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(-100%, 0, 0); + } +} +@keyframes slideOutRight { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(100%, 0, 0); + } +} +@keyframes slideOutUp { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, -100%, 0); + } +} + + + +.flipInX { + backface-visibility: visible !important; + animation-name: flipInX; +} +.flipInY { + backface-visibility: visible !important; + animation-name: flipInY; +} +.fadeIn { + animation-name: fadeIn; +} +.bounceIn { + animation-duration: 0.75s; + animation-name: bounceIn; +} +.lightSpeedIn { + animation-name: lightSpeedIn; + animation-timing-function: ease-out; +} +.rotateIn { + animation-name: rotateIn; +} +.rotateInDownLeft { + animation-name: rotateInDownLeft; +} +.rotateInDownRight { + animation-name: rotateInDownRight; +} +.rotateInUpLeft { + animation-name: rotateInUpLeft; +} +.rotateInUpRight { + animation-name: rotateInUpRight; +} +.rollIn { + animation-name: rollIn; +} +.zoomIn { + animation-name: zoomIn; +} +.zoomInDown { + animation-name: zoomInDown; +} +.zoomInLeft { + animation-name: zoomInLeft; +} +.zoomInRight { + animation-name: zoomInRight; +} +.zoomInUp { + animation-name: zoomInUp; +} +.slideInDown { + animation-name: slideInDown; +} +.slideInLeft { + animation-name: slideInLeft; +} +.slideInRight { + animation-name: slideInRight; +} +.slideInUp { + animation-name: slideInUp; +} + + +.flipOutX { + animation-duration: 0.75s; + animation-name: flipOutX; + backface-visibility: visible !important; +} +.flipOutY { + animation-duration: 0.75s; + backface-visibility: visible !important; + animation-name: flipOutY; +} +.fadeOut { + animation-name: fadeOut; +} +.bounceOut { + animation-duration: 0.75s; + animation-name: bounceOut; +} +.lightSpeedOut { + animation-name: lightSpeedOut; + animation-timing-function: ease-in; +} +.rotateOut { + animation-name: rotateOut; +} +.rotateOutDownLeft { + animation-name: rotateOutDownLeft; +} +.rotateOutDownRight { + animation-name: rotateOutDownRight; +} +.rotateOutUpLeft { + animation-name: rotateOutUpLeft; +} +.rotateOutUpRight { + animation-name: rotateOutUpRight; +} +.hinge { + animation-duration: 2s; + animation-name: hinge; +} +.rollOut { + animation-name: rollOut; +} +.zoomOut { + animation-name: zoomOut; +} +.zoomOutDown { + animation-name: zoomOutDown; +} +.zoomOutLeft { + animation-name: zoomOutLeft; +} +.zoomOutRight { + animation-name: zoomOutRight; +} +.zoomOutUp { + animation-name: zoomOutUp; +} +.slideOutDown { + animation-name: slideOutDown; +} +.slideOutLeft { + animation-name: slideOutLeft; +} +.slideOutRight { + animation-name: slideOutRight; +} +.slideOutUp { + animation-name: slideOutUp; +} + +.animated { + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animated-slow { + animation-duration: 0.8s; + animation-fill-mode: both; +} + +.animated-fast { + animation-duration: 0.3s; + animation-fill-mode: both; +} diff --git a/src/renderer/assets/styles/colors.less b/src/renderer/assets/styles/colors.less new file mode 100644 index 0000000000..ee810288ed --- /dev/null +++ b/src/renderer/assets/styles/colors.less @@ -0,0 +1,325 @@ +@red-50: #ffebee; +@red-100: #ffcdd2; +@red-200: #ef9a9a; +@red-300: #e57373; +@red-400: #ef5350; +@red-500: #f44336; +@red-600: #e53935; +@red-700: #d32f2f; +@red-800: #c62828; +@red-900: #b71c1c; +@red-A100: #ff8a80; +@red-A200: #ff5252; +@red-A400: #ff1744; +@red-A700: #d50000; +@red: @red-500; + + +@pink-50: #fce4ec; +@pink-100: #f8bbd0; +@pink-200: #f48fb1; +@pink-300: #f06292; +@pink-400: #ec407a; +@pink-500: #e91e63; +@pink-600: #d81b60; +@pink-700: #c2185b; +@pink-800: #ad1457; +@pink-900: #880e4f; +@pink-A100: #ff80ab; +@pink-A200: #ff4081; +@pink-A400: #f50057; +@pink-A700: #c51162; +@pink: @pink-500; + + +@purple-50: #f3e5f5; +@purple-100: #e1bee7; +@purple-200: #ce93d8; +@purple-300: #ba68c8; +@purple-400: #ab47bc; +@purple-500: #9c27b0; +@purple-600: #8e24aa; +@purple-700: #7b1fa2; +@purple-800: #6a1b9a; +@purple-900: #4a148c; +@purple-A100: #ea80fc; +@purple-A200: #e040fb; +@purple-A400: #d500f9; +@purple-A700: #aa00ff; +@purple: @purple-500; + + +@deep-purple-50: #ede7f6; +@deep-purple-100: #d1c4e9; +@deep-purple-200: #b39ddb; +@deep-purple-300: #9575cd; +@deep-purple-400: #7e57c2; +@deep-purple-500: #673ab7; +@deep-purple-600: #5e35b1; +@deep-purple-700: #512da8; +@deep-purple-800: #4527a0; +@deep-purple-900: #311b92; +@deep-purple-A100: #b388ff; +@deep-purple-A200: #7c4dff; +@deep-purple-A400: #651fff; +@deep-purple-A700: #6200ea; +@deep-purple: @deep-purple-500; + + +@indigo-50: #e8eaf6; +@indigo-100: #c5cae9; +@indigo-200: #9fa8da; +@indigo-300: #7986cb; +@indigo-400: #5c6bc0; +@indigo-500: #3f51b5; +@indigo-600: #3949ab; +@indigo-700: #303f9f; +@indigo-800: #283593; +@indigo-900: #1a237e; +@indigo-A100: #8c9eff; +@indigo-A200: #536dfe; +@indigo-A400: #3d5afe; +@indigo-A700: #304ffe; +@indigo: @indigo-500; + + +@blue-50: #e3f2fd; +@blue-100: #bbdefb; +@blue-200: #90caf9; +@blue-300: #64b5f6; +@blue-400: #42a5f5; +@blue-500: #2196f3; +@blue-600: #1e88e5; +@blue-700: #1976d2; +@blue-800: #1565c0; +@blue-900: #0d47a1; +@blue-A100: #82b1ff; +@blue-A200: #448aff; +@blue-A400: #2979ff; +@blue-A700: #2962ff; +@blue: @blue-500; + + +@light-blue-50: #e1f5fe; +@light-blue-100: #b3e5fc; +@light-blue-200: #81d4fa; +@light-blue-300: #4fc3f7; +@light-blue-400: #29b6f6; +@light-blue-500: #03a9f4; +@light-blue-600: #039be5; +@light-blue-700: #0288d1; +@light-blue-800: #0277bd; +@light-blue-900: #01579b; +@light-blue-A100: #80d8ff; +@light-blue-A200: #40c4ff; +@light-blue-A400: #00b0ff; +@light-blue-A700: #0091ea; +@light-blue: @light-blue-500; + + +@cyan-50: #e0f7fa; +@cyan-100: #b2ebf2; +@cyan-200: #80deea; +@cyan-300: #4dd0e1; +@cyan-400: #26c6da; +@cyan-500: #00bcd4; +@cyan-600: #00acc1; +@cyan-700: #0097a7; +@cyan-800: #00838f; +@cyan-900: #006064; +@cyan-A100: #84ffff; +@cyan-A200: #18ffff; +@cyan-A400: #00e5ff; +@cyan-A700: #00b8d4; +@cyan: @cyan-500; + + +@teal-50: #e0f2f1; +@teal-100: #b2dfdb; +@teal-200: #80cbc4; +@teal-300: #4db6ac; +@teal-400: #26a69a; +@teal-500: #009688; +@teal-600: #00897b; +@teal-700: #00796b; +@teal-800: #00695c; +@teal-900: #004d40; +@teal-A100: #a7ffeb; +@teal-A200: #64ffda; +@teal-A400: #1de9b6; +@teal-A700: #00bfa5; +@teal: @teal-500; + + +@green-50: #e8f5e9; +@green-100: #c8e6c9; +@green-200: #a5d6a7; +@green-300: #81c784; +@green-400: #66bb6a; +@green-500: #4caf50; +@green-600: #43a047; +@green-700: #388e3c; +@green-800: #2e7d32; +@green-900: #1b5e20; +@green-A100: #b9f6ca; +@green-A200: #69f0ae; +@green-A400: #00e676; +@green-A700: #00c853; +@green: @green-500; + + +@light-green-50: #f1f8e9; +@light-green-100: #dcedc8; +@light-green-200: #c5e1a5; +@light-green-300: #aed581; +@light-green-400: #9ccc65; +@light-green-500: #8bc34a; +@light-green-600: #7cb342; +@light-green-700: #689f38; +@light-green-800: #558b2f; +@light-green-900: #33691e; +@light-green-A100: #ccff90; +@light-green-A200: #b2ff59; +@light-green-A400: #76ff03; +@light-green-A700: #64dd17; +@light-green: @light-green-500; + + +@lime-50: #f9fbe7; +@lime-100: #f0f4c3; +@lime-200: #e6ee9c; +@lime-300: #dce775; +@lime-400: #d4e157; +@lime-500: #cddc39; +@lime-600: #c0ca33; +@lime-700: #afb42b; +@lime-800: #9e9d24; +@lime-900: #827717; +@lime-A100: #f4ff81; +@lime-A200: #eeff41; +@lime-A400: #c6ff00; +@lime-A700: #aeea00; +@lime: @lime-500; + + +@yellow-50: #fffde7; +@yellow-100: #fff9c4; +@yellow-200: #fff59d; +@yellow-300: #fff176; +@yellow-400: #ffee58; +@yellow-500: #fec60a; +@yellow-600: #fdd835; +@yellow-700: #fbc02d; +@yellow-800: #f9a825; +@yellow-900: #f57f17; +@yellow-A100: #ffff8d; +@yellow-A200: #ffff00; +@yellow-A400: #ffea00; +@yellow-A700: #ffd600; +@yellow: @yellow-700; + + +@amber-50: #fff8e1; +@amber-100: #ffecb3; +@amber-200: #ffe082; +@amber-300: #ffd54f; +@amber-400: #ffca28; +@amber-500: #ffc107; +@amber-600: #ffb300; +@amber-700: #ffa000; +@amber-800: #ff8f00; +@amber-900: #ff6f00; +@amber-A100: #ffe57f; +@amber-A200: #ffd740; +@amber-A400: #ffc400; +@amber-A700: #ffab00; +@amber: @amber-500; + + +@orange-50: #fff3e0; +@orange-100: #ffe0b2; +@orange-200: #ffcc80; +@orange-300: #ffb74d; +@orange-400: #ffa726; +@orange-500: #ff9800; +@orange-600: #fb8c00; +@orange-700: #f57c00; +@orange-800: #ef6c00; +@orange-900: #e65100; +@orange-A100: #ffd180; +@orange-A200: #ffab40; +@orange-A400: #ff9100; +@orange-A700: #ff6d00; +@orange: @orange-500; + + +@deep-orange-50: #fbe9e7; +@deep-orange-100: #ffccbc; +@deep-orange-200: #ffab91; +@deep-orange-300: #ff8a65; +@deep-orange-400: #ff7043; +@deep-orange-500: #ff5722; +@deep-orange-600: #f4511e; +@deep-orange-700: #e64a19; +@deep-orange-800: #d84315; +@deep-orange-900: #bf360c; +@deep-orange-A100: #ff9e80; +@deep-orange-A200: #ff6e40; +@deep-orange-A400: #ff3d00; +@deep-orange-A700: #dd2c00; +@deep-orange: @deep-orange-500; + + +@brown-50: #efebe9; +@brown-100: #d7ccc8; +@brown-200: #bcaaa4; +@brown-300: #a1887f; +@brown-400: #8d6e63; +@brown-500: #795548; +@brown-600: #6d4c41; +@brown-700: #5d4037; +@brown-800: #4e342e; +@brown-900: #3e2723; +@brown-A100: #d7ccc8; +@brown-A200: #bcaaa4; +@brown-A400: #8d6e63; +@brown-A700: #5d4037; +@brown: @brown-500; + + +@grey-50: #fafafa; +@grey-100: #f5f5f5; +@grey-200: #eeeeee; +@grey-300: #e0e0e0; +@grey-400: #bdbdbd; +@grey-500: #9e9e9e; @rgb-grey-500: "158, 158, 158"; +@grey-600: #757575; +@grey-700: #616161; +@grey-800: #424242; +@grey-900: #212121; +@grey-A100: #f5f5f5; +@grey-A200: #eeeeee; +@grey-A400: #bdbdbd; +@grey-A700: #616161; +@grey: @grey-500; + + +@blue-grey-50: #eceff1; +@blue-grey-100: #cfd8dc; +@blue-grey-200: #b0bec5; +@blue-grey-300: #90a4ae; +@blue-grey-400: #78909c; +@blue-grey-500: #607d8b; +@blue-grey-600: #546e7a; +@blue-grey-700: #455a64; +@blue-grey-800: #37474f; +@blue-grey-900: #263238; +@blue-grey-A100: #cfd8dc; +@blue-grey-A200: #b0bec5; +@blue-grey-A400: #78909c; +@blue-grey-A700: #455a64; +@blue-grey: @blue-grey-500; + + +@black: #000000; @rgb-black: "0,0,0"; +@white: #ffffff; @rgb-white: "255,255,255"; diff --git a/src/renderer/assets/styles/index.less b/src/renderer/assets/styles/index.less new file mode 100644 index 0000000000..b40b46ef73 --- /dev/null +++ b/src/renderer/assets/styles/index.less @@ -0,0 +1,165 @@ +@import './reset.less'; +@import './animate.less'; +*, *::after, *::before { + -webkit-user-drag: none; +} + +.nobreak { + white-space: nowrap; +} + +.auto-hidden { + .mixin-ellipsis-1; +} + +.break { + word-break: break-all; +} + +table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + overflow: hidden; + color: rgba(0, 0, 0, 0.87); + th { + font-size: 12px; + text-align: left; + height: 28px; + line-height: 28px; + padding: 5px; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + } + tbody { + tr { + border-top: 1px solid #e0e0e0; + // border-top: 1px solid rgba(0, 0, 0, 0.12); + transition: background-color 0.2s ease; + &:hover { + background-color: #eee; + } + &:first-child { + border-top: none; + } + td { + padding: 5px; + position: relative; + transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 13px; + line-height: 1.3; + vertical-align: middle; + } + } + } +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: .7em; + // font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 2px; + + &.badge-light { + background-color: #f8f9fa; + } + &.badge-secondary { + color: #fff; + background-color: #6c757d; + } + &.badge-info { + color: #fff; + background-color: #4baed5; + } + &.badge-warning { + color: #fff; + background-color: #ffa45a; + } + &.badge-danger { + color: #fff; + background-color: #ff705a; + } + &.badge-success { + color: #fff; + background-color: #32bc63; + } +} + +small { + font-size: .8em; +} +strong { + font-weight: bold; +} + +svg { + transition: @transition-theme; + transition-property: fill; +} + +.hover { + cursor: pointer; + transition: color .2s ease; + &:hover { + color: @color-theme; + } + &:active { + color: @color-theme-active; + } +} + +.scroll { + &::-webkit-scrollbar { + width: 6px; + height: 6px; + background-color: rgba(0, 0, 0, 0); + } + &::-webkit-scrollbar-track { + background-color: @color-scrollbar-track; + border-radius: 3px; + // background-color: rgba(0, 0, 0, 0.1); + } + &::-webkit-scrollbar-thumb { + border-radius: 3px; + background-color: @color-scrollbar-thumb; + // background-color: rgba(0, 0, 0, 0.2); + transition: all 0.4s ease; + } + &::-webkit-scrollbar-thumb:hover { + border-radius: 3px; + background-color: @color-scrollbar-thumb-hover; + // background-color: rgba(0, 0, 0, 0.4); + transition: all 0.4s ease; + } +} + +each(@themes, { + #container.@{value} { + .hover { + &:hover { + color: ~'@{color-@{value}-theme}'; + } + &:active { + color: ~'@{color-@{value}-theme-active}'; + } + } + .scroll { + &::-webkit-scrollbar { + background-color: rgba(0, 0, 0, 0); + } + &::-webkit-scrollbar-track { + background-color: ~'@{color-@{value}-scrollbar-track}'; + } + &::-webkit-scrollbar-thumb { + background-color: ~'@{color-@{value}-scrollbar-thumb}'; + } + &::-webkit-scrollbar-thumb:hover { + background-color: ~'@{color-@{value}-scrollbar-thumb-hover}'; + } + } + } +}) diff --git a/src/renderer/assets/styles/layout.less b/src/renderer/assets/styles/layout.less new file mode 100644 index 0000000000..8c249a341d --- /dev/null +++ b/src/renderer/assets/styles/layout.less @@ -0,0 +1,30 @@ +@import './variables.less'; + + +/*自动隐藏文字*/ +.mixin-ellipsis-1() { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.mixin-ellipsis() { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} +.mixin-ellipsis-2() { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + diff --git a/src/renderer/assets/styles/reset.less b/src/renderer/assets/styles/reset.less new file mode 100644 index 0000000000..0e5f2e0c1d --- /dev/null +++ b/src/renderer/assets/styles/reset.less @@ -0,0 +1,43 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/renderer/assets/styles/variables.less b/src/renderer/assets/styles/variables.less new file mode 100644 index 0000000000..2ca81a5d99 --- /dev/null +++ b/src/renderer/assets/styles/variables.less @@ -0,0 +1,288 @@ +@import './colors.less'; + +@themes: green, yellow, blue, red, purple, orange, grey; + + +// Colors +// @color-theme: #03a678; +@color-theme: #4daf7c; +@color-theme-hover: fadeout(lighten(@color-theme, 10%), 30%); +@color-theme-active: fadeout(darken(@color-theme, 20%), 60%); +@color-theme-font: #fff; +@color-theme-font-label: lighten(@color-theme, 35%); + +@color-theme_2: #fff; +@color-theme_2-hover: fadeout(lighten(@color-theme, 10%), 70%); +@color-theme_2-active: fadeout(darken(@color-theme, 5%), 70%); +@color-theme_2-font: darken(@color-theme_2, 70%); +@color-theme_2-font-label: lighten(@color-theme, 40%); +@color-btn: fadeout(darken(@color-theme, 5%), 15%); +@color-btn-background: fadeout(lighten(@color-theme, 35%), 70%); + +@color-pagination-background: fadeout(lighten(@color-theme, 45%), 30%); +@color-pagination-hover: fadeout(lighten(@color-theme, 10%), 70%); +@color-pagination-active: fadeout(darken(@color-theme, 10%), 70%); +@color-pagination-select: fadeout(lighten(@color-theme, 10%), 50%); + +@color-search-form-background: fadeout(lighten(@color-theme, 35%), 10%); +// @color-search-list-background: fadeout(lighten(@color-theme, 35%), 10%); +@color-search-list-hover: fadeout(darken(@color-theme, 10%), 70%); + +@color-scrollbar-track: fadeout(@color-theme, 80%); +@color-scrollbar-thumb: fadeout(@color-theme, 60%); +@color-scrollbar-thumb-hover: fadeout(@color-theme, 40%); + + +@color-player-pic-c1: fadeout(@color-theme_2, 50%); +@color-player-pic-c2: darken(@color-theme_2, 30%); +@color-player-progress: darken(@color-theme_2, 6%); +@color-player-progress-bar1: darken(@color-theme_2, 12%); +@color-player-progress-bar2: lighten(@color-theme, 12%); +@color-player-status-text: lighten(@color-theme_2-font, 10%); + +@color-tab-btn-background: fadeout(lighten(@color-theme, 10%), 80%); +@color-tab-border-top: fadeout(lighten(@color-theme, 5%), 50%); +@color-tab-border-bottom: lighten(@color-theme, 5%); + +@color-minBtn: #85c43b; +@color-maxBtn: #e7aa36; +@color-closeBtn: #ea6e4d; + + +@color-green-theme: #4daf7c; +@color-green-theme-hover: fadeout(lighten(@color-green-theme, 10%), 30%); +@color-green-theme-active: fadeout(darken(@color-green-theme, 20%), 60%); +@color-green-theme-font: #fff; +@color-green-theme-font-label: lighten(@color-green-theme, 35%); +@color-green-theme_2: #fff; +@color-green-theme_2-hover: fadeout(lighten(@color-green-theme, 10%), 70%); +@color-green-theme_2-active: fadeout(darken(@color-green-theme, 5%), 70%); +@color-green-theme_2-font: darken(@color-green-theme_2, 70%); +@color-green-theme_2-font-label: lighten(@color-green-theme, 40%); +@color-green-btn: fadeout(darken(@color-green-theme, 5%), 15%); +@color-green-btn-background: fadeout(lighten(@color-green-theme, 35%), 70%); +@color-green-pagination-background: fadeout(lighten(@color-green-theme, 45%), 30%); +@color-green-pagination-hover: fadeout(lighten(@color-green-theme, 10%), 70%); +@color-green-pagination-active: fadeout(darken(@color-green-theme, 10%), 70%); +@color-green-pagination-select: fadeout(lighten(@color-green-theme, 10%), 50%); +@color-green-search-form-background: fadeout(lighten(@color-green-theme, 35%), 10%); +@color-green-search-list-hover: fadeout(darken(@color-green-theme, 10%), 70%); +@color-green-scrollbar-track: fadeout(@color-green-theme, 80%); +@color-green-scrollbar-thumb: fadeout(@color-green-theme, 60%); +@color-green-scrollbar-thumb-hover: fadeout(@color-green-theme, 40%); +@color-green-player-pic-c1: fadeout(@color-green-theme_2, 50%); +@color-green-player-pic-c2: darken(@color-green-theme_2, 30%); +@color-green-player-progress: darken(@color-green-theme_2, 6%); +@color-green-player-progress-bar1: darken(@color-green-theme_2, 12%); +@color-green-player-progress-bar2: lighten(@color-green-theme, 12%); +@color-green-player-status-text: lighten(@color-green-theme_2-font, 10%); +@color-green-tab-btn-background: fadeout(lighten(@color-green-theme, 10%), 80%); +@color-green-tab-border-top: fadeout(lighten(@color-green-theme, 5%), 50%); +@color-green-tab-border-bottom: lighten(@color-green-theme, 5%); + + +@color-yellow-theme: #f2d35b; +@color-yellow-theme-hover: fadeout(lighten(@color-yellow-theme, 10%), 30%); +@color-yellow-theme-active: fadeout(darken(@color-yellow-theme, 20%), 60%); +@color-yellow-theme-font: #fff; +@color-yellow-theme-font-label: lighten(@color-yellow-theme, 35%); +@color-yellow-theme_2: #fff; +@color-yellow-theme_2-hover: fadeout(lighten(@color-yellow-theme, 10%), 70%); +@color-yellow-theme_2-active: fadeout(darken(@color-yellow-theme, 5%), 70%); +@color-yellow-theme_2-font: darken(@color-yellow-theme_2, 70%); +@color-yellow-theme_2-font-label: lighten(@color-yellow-theme, 40%); +@color-yellow-btn: fadeout(darken(@color-yellow-theme, 5%), 15%); +@color-yellow-btn-background: fadeout(lighten(@color-yellow-theme, 25%), 70%); +@color-yellow-pagination-background: fadeout(lighten(@color-yellow-theme, 30%), 30%); +@color-yellow-pagination-hover: fadeout(lighten(@color-yellow-theme, 5%), 70%); +@color-yellow-pagination-active: fadeout(darken(@color-yellow-theme, 5%), 70%); +@color-yellow-pagination-select: fadeout(lighten(@color-yellow-theme, 5%), 50%); +@color-yellow-search-form-background: fadeout(lighten(@color-yellow-theme, 35%), 10%); +@color-yellow-search-list-hover: fadeout(darken(@color-yellow-theme, 10%), 70%); +@color-yellow-scrollbar-track: fadeout(@color-yellow-theme, 80%); +@color-yellow-scrollbar-thumb: fadeout(@color-yellow-theme, 60%); +@color-yellow-scrollbar-thumb-hover: fadeout(@color-yellow-theme, 40%); +@color-yellow-player-pic-c1: fadeout(@color-yellow-theme_2, 50%); +@color-yellow-player-pic-c2: darken(@color-yellow-theme_2, 30%); +@color-yellow-player-progress: darken(@color-yellow-theme_2, 6%); +@color-yellow-player-progress-bar1: darken(@color-yellow-theme_2, 12%); +@color-yellow-player-progress-bar2: lighten(@color-yellow-theme, 12%); +@color-yellow-player-status-text: lighten(@color-yellow-theme_2-font, 10%); +@color-yellow-tab-btn-background: fadeout(lighten(@color-yellow-theme, 10%), 80%); +@color-yellow-tab-border-top: fadeout(lighten(@color-yellow-theme, 5%), 50%); +@color-yellow-tab-border-bottom: lighten(@color-yellow-theme, 5%); + +@color-orange-theme: #f5ab35; +@color-orange-theme-hover: fadeout(lighten(@color-orange-theme, 10%), 30%); +@color-orange-theme-active: fadeout(darken(@color-orange-theme, 20%), 60%); +@color-orange-theme-font: #fff; +@color-orange-theme-font-label: lighten(@color-orange-theme, 35%); +@color-orange-theme_2: #fff; +@color-orange-theme_2-hover: fadeout(lighten(@color-orange-theme, 10%), 70%); +@color-orange-theme_2-active: fadeout(darken(@color-orange-theme, 5%), 70%); +@color-orange-theme_2-font: darken(@color-orange-theme_2, 70%); +@color-orange-theme_2-font-label: lighten(@color-orange-theme, 40%); +@color-orange-btn: fadeout(darken(@color-orange-theme, 5%), 15%); +@color-orange-btn-background: fadeout(lighten(@color-orange-theme, 35%), 70%); +@color-orange-pagination-background: fadeout(lighten(@color-orange-theme, 35%), 30%); +@color-orange-pagination-hover: fadeout(lighten(@color-orange-theme, 10%), 70%); +@color-orange-pagination-active: fadeout(darken(@color-orange-theme, 10%), 70%); +@color-orange-pagination-select: fadeout(lighten(@color-orange-theme, 10%), 50%); +@color-orange-search-form-background: fadeout(lighten(@color-orange-theme, 35%), 10%); +@color-orange-search-list-hover: fadeout(darken(@color-orange-theme, 10%), 70%); +@color-orange-scrollbar-track: fadeout(@color-orange-theme, 80%); +@color-orange-scrollbar-thumb: fadeout(@color-orange-theme, 60%); +@color-orange-scrollbar-thumb-hover: fadeout(@color-orange-theme, 40%); +@color-orange-player-pic-c1: fadeout(@color-orange-theme_2, 50%); +@color-orange-player-pic-c2: darken(@color-orange-theme_2, 30%); +@color-orange-player-progress: darken(@color-orange-theme_2, 6%); +@color-orange-player-progress-bar1: darken(@color-orange-theme_2, 12%); +@color-orange-player-progress-bar2: lighten(@color-orange-theme, 12%); +@color-orange-player-status-text: lighten(@color-orange-theme_2-font, 10%); +@color-orange-tab-btn-background: fadeout(lighten(@color-orange-theme, 10%), 80%); +@color-orange-tab-border-top: fadeout(lighten(@color-orange-theme, 5%), 50%); +@color-orange-tab-border-bottom: lighten(@color-orange-theme, 5%); + +@color-blue-theme: #3498db; +@color-blue-theme-hover: fadeout(lighten(@color-blue-theme, 10%), 30%); +@color-blue-theme-active: fadeout(darken(@color-blue-theme, 20%), 60%); +@color-blue-theme-font: #fff; +@color-blue-theme-font-label: lighten(@color-blue-theme, 35%); +@color-blue-theme_2: #fff; +@color-blue-theme_2-hover: fadeout(lighten(@color-blue-theme, 10%), 70%); +@color-blue-theme_2-active: fadeout(darken(@color-blue-theme, 5%), 70%); +@color-blue-theme_2-font: darken(@color-blue-theme_2, 70%); +@color-blue-theme_2-font-label: lighten(@color-blue-theme, 40%); +@color-blue-btn: fadeout(darken(@color-blue-theme, 5%), 15%); +@color-blue-btn-background: fadeout(lighten(@color-blue-theme, 35%), 70%); +@color-blue-pagination-background: fadeout(lighten(@color-blue-theme, 40%), 30%); +@color-blue-pagination-hover: fadeout(lighten(@color-blue-theme, 15%), 70%); +@color-blue-pagination-active: fadeout(darken(@color-blue-theme, 15%), 70%); +@color-blue-pagination-select: fadeout(lighten(@color-blue-theme, 15%), 50%); +@color-blue-search-form-background: fadeout(lighten(@color-blue-theme, 35%), 10%); +@color-blue-search-list-hover: fadeout(darken(@color-blue-theme, 10%), 70%); +@color-blue-scrollbar-track: fadeout(@color-blue-theme, 80%); +@color-blue-scrollbar-thumb: fadeout(@color-blue-theme, 60%); +@color-blue-scrollbar-thumb-hover: fadeout(@color-blue-theme, 40%); +@color-blue-player-pic-c1: fadeout(@color-blue-theme_2, 50%); +@color-blue-player-pic-c2: darken(@color-blue-theme_2, 30%); +@color-blue-player-progress: darken(@color-blue-theme_2, 6%); +@color-blue-player-progress-bar1: darken(@color-blue-theme_2, 12%); +@color-blue-player-progress-bar2: lighten(@color-blue-theme, 12%); +@color-blue-player-status-text: lighten(@color-blue-theme_2-font, 10%); +@color-blue-tab-btn-background: fadeout(lighten(@color-blue-theme, 10%), 80%); +@color-blue-tab-border-top: fadeout(lighten(@color-blue-theme, 5%), 50%); +@color-blue-tab-border-bottom: lighten(@color-blue-theme, 5%); + +@color-red-theme: #d64541; +@color-red-theme-hover: fadeout(lighten(@color-red-theme, 10%), 30%); +@color-red-theme-active: fadeout(darken(@color-red-theme, 20%), 60%); +@color-red-theme-font: #fff; +@color-red-theme-font-label: lighten(@color-red-theme, 35%); +@color-red-theme_2: #fff; +@color-red-theme_2-hover: fadeout(lighten(@color-red-theme, 10%), 70%); +@color-red-theme_2-active: fadeout(darken(@color-red-theme, 5%), 70%); +@color-red-theme_2-font: darken(@color-red-theme_2, 70%); +@color-red-theme_2-font-label: lighten(@color-red-theme, 40%); +@color-red-btn: fadeout(darken(@color-red-theme, 5%), 15%); +@color-red-btn-background: fadeout(lighten(@color-red-theme, 35%), 70%); +@color-red-pagination-background: fadeout(lighten(@color-red-theme, 40%), 30%); +@color-red-pagination-hover: fadeout(lighten(@color-red-theme, 15%), 70%); +@color-red-pagination-active: fadeout(darken(@color-red-theme, 15%), 70%); +@color-red-pagination-select: fadeout(lighten(@color-red-theme, 15%), 50%); +@color-red-search-form-background: fadeout(lighten(@color-red-theme, 35%), 10%); +@color-red-search-list-hover: fadeout(darken(@color-red-theme, 10%), 70%); +@color-red-scrollbar-track: fadeout(@color-red-theme, 80%); +@color-red-scrollbar-thumb: fadeout(@color-red-theme, 60%); +@color-red-scrollbar-thumb-hover: fadeout(@color-red-theme, 40%); +@color-red-player-pic-c1: fadeout(@color-red-theme_2, 50%); +@color-red-player-pic-c2: darken(@color-red-theme_2, 30%); +@color-red-player-progress: darken(@color-red-theme_2, 6%); +@color-red-player-progress-bar1: darken(@color-red-theme_2, 12%); +@color-red-player-progress-bar2: lighten(@color-red-theme, 12%); +@color-red-player-status-text: lighten(@color-red-theme_2-font, 10%); +@color-red-tab-border-top: fadeout(lighten(@color-red-theme, 25%), 70%); +@color-red-tab-border-bottom: lighten(@color-red-theme, 35%); +@color-red-tab-btn-background: fadeout(lighten(@color-red-theme, 10%), 80%); +@color-red-tab-border-top: fadeout(lighten(@color-red-theme, 5%), 50%); +@color-red-tab-border-bottom: lighten(@color-red-theme, 5%); + +@color-purple-theme: #9b59b6; +@color-purple-theme-hover: fadeout(lighten(@color-purple-theme, 10%), 30%); +@color-purple-theme-active: fadeout(darken(@color-purple-theme, 20%), 60%); +@color-purple-theme-font: #fff; +@color-purple-theme-font-label: lighten(@color-purple-theme, 35%); +@color-purple-theme_2: #fff; +@color-purple-theme_2-hover: fadeout(lighten(@color-purple-theme, 10%), 70%); +@color-purple-theme_2-active: fadeout(darken(@color-purple-theme, 5%), 70%); +@color-purple-theme_2-font: darken(@color-purple-theme_2, 70%); +@color-purple-theme_2-font-label: lighten(@color-purple-theme, 40%); +@color-purple-btn: fadeout(darken(@color-purple-theme, 5%), 15%); +@color-purple-btn-background: fadeout(lighten(@color-purple-theme, 35%), 70%); +@color-purple-pagination-background: fadeout(lighten(@color-purple-theme, 40%), 30%); +@color-purple-pagination-hover: fadeout(lighten(@color-purple-theme, 15%), 70%); +@color-purple-pagination-active: fadeout(darken(@color-purple-theme, 15%), 70%); +@color-purple-pagination-select: fadeout(lighten(@color-purple-theme, 15%), 50%); +@color-purple-search-form-background: fadeout(lighten(@color-purple-theme, 35%), 10%); +@color-purple-search-list-hover: fadeout(darken(@color-purple-theme, 10%), 70%); +@color-purple-scrollbar-track: fadeout(@color-purple-theme, 80%); +@color-purple-scrollbar-thumb: fadeout(@color-purple-theme, 60%); +@color-purple-scrollbar-thumb-hover: fadeout(@color-purple-theme, 40%); +@color-purple-player-pic-c1: fadeout(@color-purple-theme_2, 50%); +@color-purple-player-pic-c2: darken(@color-purple-theme_2, 30%); +@color-purple-player-progress: darken(@color-purple-theme_2, 6%); +@color-purple-player-progress-bar1: darken(@color-purple-theme_2, 12%); +@color-purple-player-progress-bar2: lighten(@color-purple-theme, 12%); +@color-purple-player-status-text: lighten(@color-purple-theme_2-font, 10%); +@color-purple-tab-btn-background: fadeout(lighten(@color-purple-theme, 10%), 80%); +@color-purple-tab-border-top: fadeout(lighten(@color-purple-theme, 5%), 50%); +@color-purple-tab-border-bottom: lighten(@color-purple-theme, 5%); + +@color-grey-theme: #6c7a89; +@color-grey-theme-hover: fadeout(lighten(@color-grey-theme, 10%), 30%); +@color-grey-theme-active: fadeout(darken(@color-grey-theme, 20%), 60%); +@color-grey-theme-font: #fff; +@color-grey-theme-font-label: lighten(@color-grey-theme, 35%); +@color-grey-theme_2: #fff; +@color-grey-theme_2-hover: fadeout(lighten(@color-grey-theme, 10%), 70%); +@color-grey-theme_2-active: fadeout(darken(@color-grey-theme, 5%), 70%); +@color-grey-theme_2-font: darken(@color-grey-theme_2, 70%); +@color-grey-theme_2-font-label: lighten(@color-grey-theme, 40%); +@color-grey-btn: fadeout(darken(@color-grey-theme, 5%), 15%); +@color-grey-btn-background: fadeout(lighten(@color-grey-theme, 35%), 70%); +@color-grey-pagination-background: fadeout(lighten(@color-grey-theme, 45%), 30%); +@color-grey-pagination-hover: fadeout(lighten(@color-grey-theme, 10%), 70%); +@color-grey-pagination-active: fadeout(darken(@color-grey-theme, 10%), 70%); +@color-grey-pagination-select: fadeout(lighten(@color-grey-theme, 10%), 50%); +@color-grey-search-form-background: fadeout(lighten(@color-grey-theme, 35%), 10%); +@color-grey-search-list-hover: fadeout(darken(@color-grey-theme, 10%), 70%); +@color-grey-scrollbar-track: fadeout(@color-grey-theme, 80%); +@color-grey-scrollbar-thumb: fadeout(@color-grey-theme, 60%); +@color-grey-scrollbar-thumb-hover: fadeout(@color-grey-theme, 40%); +@color-grey-player-pic-c1: fadeout(@color-grey-theme_2, 50%); +@color-grey-player-pic-c2: darken(@color-grey-theme_2, 30%); +@color-grey-player-progress: darken(@color-grey-theme_2, 6%); +@color-grey-player-progress-bar1: darken(@color-grey-theme_2, 12%); +@color-grey-player-progress-bar2: lighten(@color-grey-theme, 12%); +@color-grey-player-status-text: lighten(@color-grey-theme_2-font, 10%); +@color-grey-tab-btn-background: fadeout(lighten(@color-grey-theme, 10%), 80%); +@color-grey-tab-border-top: fadeout(lighten(@color-grey-theme, 5%), 50%); +@color-grey-tab-border-bottom: lighten(@color-grey-theme, 5%); + + +// Width +@width-app-left: 180px; + +// Height +@height-toolbar: 50px; +@height-player: 55px; + + +// Shadow +@shadow-app: 8px; + + +// Radius +@radius-progress-border: 5px; + +@transition-theme: .4s ease; diff --git a/src/renderer/components/core/Aside.vue b/src/renderer/components/core/Aside.vue new file mode 100644 index 0000000000..50e1bf9ccb --- /dev/null +++ b/src/renderer/components/core/Aside.vue @@ -0,0 +1,149 @@ + + + + + + diff --git a/src/renderer/components/core/Icons.vue b/src/renderer/components/core/Icons.vue new file mode 100644 index 0000000000..1586631cea --- /dev/null +++ b/src/renderer/components/core/Icons.vue @@ -0,0 +1,49 @@ + + diff --git a/src/renderer/components/core/Player.vue b/src/renderer/components/core/Player.vue new file mode 100644 index 0000000000..8be2f536b8 --- /dev/null +++ b/src/renderer/components/core/Player.vue @@ -0,0 +1,606 @@ + + + + + + diff --git a/src/renderer/components/core/Toolbar.vue b/src/renderer/components/core/Toolbar.vue new file mode 100644 index 0000000000..bae5769d27 --- /dev/null +++ b/src/renderer/components/core/Toolbar.vue @@ -0,0 +1,128 @@ + + + + + + diff --git a/src/renderer/components/core/View.vue b/src/renderer/components/core/View.vue new file mode 100644 index 0000000000..aa5d716e18 --- /dev/null +++ b/src/renderer/components/core/View.vue @@ -0,0 +1,33 @@ + + + + diff --git a/src/renderer/components/index.js b/src/renderer/components/index.js new file mode 100644 index 0000000000..c212e18f2c --- /dev/null +++ b/src/renderer/components/index.js @@ -0,0 +1,17 @@ +import Vue from 'vue' +import upperFirst from 'lodash/upperFirst' +import camelCase from 'lodash/camelCase' + +const requireComponent = require.context( + './', true, /\.vue$/ +) + +requireComponent.keys().forEach(fileName => { + const componentConfig = requireComponent(fileName) + + const componentName = upperFirst( + camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')) + ) + + Vue.component(componentName, componentConfig.default || componentConfig) +}) diff --git a/src/renderer/components/material/Btn.vue b/src/renderer/components/material/Btn.vue new file mode 100644 index 0000000000..be8a71936e --- /dev/null +++ b/src/renderer/components/material/Btn.vue @@ -0,0 +1,59 @@ + + + + + + diff --git a/src/renderer/components/material/Checkbox.vue b/src/renderer/components/material/Checkbox.vue new file mode 100644 index 0000000000..2ddf0ec609 --- /dev/null +++ b/src/renderer/components/material/Checkbox.vue @@ -0,0 +1,160 @@ + + + + + + diff --git a/src/renderer/components/material/DownloadModal.vue b/src/renderer/components/material/DownloadModal.vue new file mode 100644 index 0000000000..239e75b88c --- /dev/null +++ b/src/renderer/components/material/DownloadModal.vue @@ -0,0 +1,94 @@ + + + + + + diff --git a/src/renderer/components/material/ListButtons.vue b/src/renderer/components/material/ListButtons.vue new file mode 100644 index 0000000000..6a5eea0dee --- /dev/null +++ b/src/renderer/components/material/ListButtons.vue @@ -0,0 +1,106 @@ + + + + + + diff --git a/src/renderer/components/material/Modal.vue b/src/renderer/components/material/Modal.vue new file mode 100644 index 0000000000..e9c66891a0 --- /dev/null +++ b/src/renderer/components/material/Modal.vue @@ -0,0 +1,199 @@ + + + + + + diff --git a/src/renderer/components/material/Pagination.vue b/src/renderer/components/material/Pagination.vue new file mode 100644 index 0000000000..bd087494d9 --- /dev/null +++ b/src/renderer/components/material/Pagination.vue @@ -0,0 +1,227 @@ + + + + + + diff --git a/src/renderer/components/material/SearchInput.vue b/src/renderer/components/material/SearchInput.vue new file mode 100644 index 0000000000..024b33bea7 --- /dev/null +++ b/src/renderer/components/material/SearchInput.vue @@ -0,0 +1,232 @@ + + + + + + diff --git a/src/renderer/components/material/Select.vue b/src/renderer/components/material/Select.vue new file mode 100644 index 0000000000..878c9d638f --- /dev/null +++ b/src/renderer/components/material/Select.vue @@ -0,0 +1,170 @@ + + + + + + diff --git a/src/renderer/components/material/Tab.vue b/src/renderer/components/material/Tab.vue new file mode 100644 index 0000000000..90e2f65628 --- /dev/null +++ b/src/renderer/components/material/Tab.vue @@ -0,0 +1,221 @@ + + + + + + diff --git a/src/renderer/components/material/VersionModal.vue b/src/renderer/components/material/VersionModal.vue new file mode 100644 index 0000000000..31655f3637 --- /dev/null +++ b/src/renderer/components/material/VersionModal.vue @@ -0,0 +1,156 @@ + + + + + + diff --git a/src/renderer/config/index.js b/src/renderer/config/index.js new file mode 100644 index 0000000000..6001e6d289 --- /dev/null +++ b/src/renderer/config/index.js @@ -0,0 +1,11 @@ +module.exports = { + development: { + + }, + production: { + + }, + ajax: { + timeout: 15000, // ajax请求超时时间 + }, +} diff --git a/src/renderer/main.js b/src/renderer/main.js new file mode 100644 index 0000000000..86203fa217 --- /dev/null +++ b/src/renderer/main.js @@ -0,0 +1,27 @@ +import Vue from 'vue' +// import { sync } from 'vuex-router-sync' + +// Components +import './components' + +// Plugins +import './plugins' + +import App from './App' +import router from './route' +import store from './store' + +// sync(store, router) + +if (!process.env.IS_WEB) { + +} + +Vue.config.productionTip = false + +new Vue({ + router, + store, + el: '#root', + render: h => h(App), +}) diff --git a/src/renderer/plugins/index.js b/src/renderer/plugins/index.js new file mode 100644 index 0000000000..bff0ea0f02 --- /dev/null +++ b/src/renderer/plugins/index.js @@ -0,0 +1 @@ +// import './axios' diff --git a/src/renderer/route/index.js b/src/renderer/route/index.js new file mode 100644 index 0000000000..bc74c7c614 --- /dev/null +++ b/src/renderer/route/index.js @@ -0,0 +1,56 @@ +import Vue from 'vue' +import Router from 'vue-router' + +import paths from './paths' + + +function route(path, view, name, meta) { + return { + name: name || view, + path, + meta, + component: (resovle) => import( + `../views/${view}.vue` + ).then(resovle), + } +} + +Vue.use(Router) + +const router = new Router({ + mode: 'hash', + routes: paths.map(path => route(path.path, path.view, path.name, path.meta)).concat([ + { path: '*', redirect: '/search' }, + ]), + linkActiveClass: 'active-link', + linkExactActiveClass: 'exact-active-link', + scrollBehavior(to, from, savedPosition) { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (savedPosition) { + resolve(savedPosition) + } else { + const position = {} + // new navigation. + // scroll to anchor by returning the selector + if (to.hash) { + position.selector = to.hash + } + // check if any matched route config has meta that requires scrolling to top + if (to.matched.some(m => m.meta.scrollToTop)) { + // cords will be used if no selector is provided, + // or if the selector didn't match any element. + position.x = 0 + position.y = 0 + } + // if the returned position is falsy or an empty object, + // will retain current scroll position. + resolve(position) + } + }, 500) + }) + }, +}) + + +export default router diff --git a/src/renderer/route/paths.js b/src/renderer/route/paths.js new file mode 100644 index 0000000000..42e1b7acab --- /dev/null +++ b/src/renderer/route/paths.js @@ -0,0 +1,40 @@ +export default [ + // { + // path: '/', + // // redirect: '/app', + // // props: true, + // component: () => import('../views/Dashboard.vue'), + // name: 'Dashboard', + // alias: '/dashboard' + // } + { + path: '/search', + name: 'search', + view: 'Search', + }, + { + path: '/leaderboard', + name: 'leaderboard', + view: 'Leaderboard', + }, + { + path: '/recommend', + name: 'recommend', + view: 'Recommend', + }, + { + path: '/list', + name: 'list', + view: 'List', + }, + { + path: '/download', + name: 'download', + view: 'Download', + }, + { + path: '/setting', + name: 'setting', + view: 'Setting', + }, +] diff --git a/src/renderer/store/actions.js b/src/renderer/store/actions.js new file mode 100644 index 0000000000..069a897d84 --- /dev/null +++ b/src/renderer/store/actions.js @@ -0,0 +1,14 @@ +// import api from 'api/connom' +import { httpGet } from '../utils/request' +import { author, name } from '../../../package.json' + +export default { + getVersionInfo() { + return new Promise((resolve, reject) => { + httpGet(`https://raw.githubusercontent.com/${author}/${name}/master/publish/version.json`, (err, resp, body) => { + if (err) return reject(err) + resolve(body) + }) + }) + }, +} diff --git a/src/renderer/store/getters.js b/src/renderer/store/getters.js new file mode 100644 index 0000000000..9a445322d2 --- /dev/null +++ b/src/renderer/store/getters.js @@ -0,0 +1,34 @@ +import music from '../utils/music' + +export default { + theme(state) { + return (state.themes[state.setting.themeId] && state.themes[state.setting.themeId].class) || '' + }, + themes(state) { + return { + active: state.setting.themeId, + list: state.themes, + } + }, + source(state) { + return music.sources.find(s => s.id === state.setting.sourceId) || music.sources[0] + }, + sources(state) { + return { + active: state.setting.sourceId, + list: music.sources, + } + }, + userInfo(state) { + return state.userInfo + }, + setting(state) { + return state.setting + }, + electronStore(state) { + return state.electronStore + }, + version(state) { + return state.version + }, +} diff --git a/src/renderer/store/index.js b/src/renderer/store/index.js new file mode 100644 index 0000000000..e73c05c1cd --- /dev/null +++ b/src/renderer/store/index.js @@ -0,0 +1,46 @@ +import Vue from 'vue' +import Vuex from 'vuex' +// import { createPersistedState, createSharedMutations } from 'vuex-electron' + +import defaultState from './state' +import mutations from './mutations' +import modules from './modules' +import getters from './getters' +import actions from './actions' + +Vue.use(Vuex) + +const isDev = process.env.NODE_ENV === 'development' + +const store = new Vuex.Store({ + strict: isDev, + state: defaultState, + modules, + mutations, + getters, + actions, + // plugins: [createPersistedState(), createSharedMutations()], +}) + +if (module.hot) { + module.hot.accept([ + './state', + './mutations', + './actions', + './getters', + ], () => { + const newState = require('./state').default + const newMutations = require('./mutations').default + const newActions = require('./actions').default + const newGetters = require('./getters').default + + store.hotUpdate({ + state: newState, + mutations: newMutations, + getters: newGetters, + actions: newActions, + }) + }) +} + +export default store diff --git a/src/renderer/store/modules/download.js b/src/renderer/store/modules/download.js new file mode 100644 index 0000000000..f441f65e10 --- /dev/null +++ b/src/renderer/store/modules/download.js @@ -0,0 +1,274 @@ +import download from '../../utils/download' +import fs from 'fs' +import path from 'path' +import music from '../../utils/music' + +// state +const state = { + list: [], +} + +const dls = {} + +// getters +const getters = { + list: state => state.list || [], + dls: () => dls || {}, +} + +const checkPath = path => { + try { + if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true }) + } catch (error) { + return error.message + } + return false +} + +const getExt = type => { + switch (type) { + case '128k': + case '192k': + case '320k': + return 'mp3' + case 'ape': + return 'ape' + case 'flac': + return 'flac' + } +} + +const checkList = (list, musicInfo, type) => list.some(s => s.musicInfo.songmid === musicInfo.songmid && s.type === type) + +const refreshUrl = downloadInfo => { + return music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type) +} + +// actions +const actions = { + createDownload({ state, rootState }, { musicInfo, type }) { + if (checkList(state.list, musicInfo, type)) return + let ext = getExt(type) + const downloadInfo = { + isComplate: false, + isDownloading: false, + statusText: '任务初始化中', + url: null, + fileName: `${rootState.setting.download.fileName + .replace('歌名', musicInfo.name) + .replace('歌手', musicInfo.singer)}.${ext}`, + progress: { + downloaded: 0, + total: 0, + progress: 0, + }, + type, + ext, + musicInfo, + key: `${musicInfo.songmid}${ext}`, + } + downloadInfo.filePath = path.join(rootState.setting.download.savePath, downloadInfo.fileName) + if (dls[downloadInfo.key]) { + dls[downloadInfo.key].stop().finally(() => { + this.dispatch('download/addTask', downloadInfo) + }) + } else { + // console.log(downloadInfo) + this.dispatch('download/addTask', downloadInfo) + } + }, + addTask({ commit, rootState }, downloadInfo) { + commit('addTask', downloadInfo) + let msg = checkPath(rootState.setting.download.savePath) + if (msg) return commit('setStatusText', '检查下载目录出错: ' + msg) + const options = { + url: downloadInfo.url, + path: rootState.setting.download.savePath, + fileName: downloadInfo.fileName, + method: 'get', + override: true, + onEnd() { + commit('onEnd', downloadInfo) + console.log('on complate') + }, + onError(err) { + console.log(err) + if (err.message.includes('Response status was')) { + const code = err.message.replace(/Response status was (\d+)$/, '$1') + switch (code) { + case '401': + case '403': + case '410': + commit('setStatusText', { downloadInfo, text: '链接失效,正在刷新链接' }) + refreshUrl(downloadInfo).then(result => { + commit('updateUrl', { downloadInfo, url: result.url }) + commit('setStatusText', { downloadInfo, text: '链接刷新成功' }) + dls[downloadInfo.key].url = dls[downloadInfo.key].requestURL = result.url + dls[downloadInfo.key].__initProtocol(result.url) + dls[downloadInfo.key].resume() + }).catch(err => { + console.log(err) + }) + return + + default: + break + } + } + commit('onError', downloadInfo) + }, + onStateChanged(state) { + console.log(state) + }, + onDownload() { + commit('onDownload', downloadInfo) + console.log('on download') + }, + onProgress(status) { + commit('onProgress', { downloadInfo, status }) + console.log(status) + }, + onPause() { + commit('pauseTask', downloadInfo) + }, + onResume() { + commit('resumeTask', downloadInfo) + }, + } + let p = options.url ? Promise.resolve() : refreshUrl(downloadInfo).then(result => { + commit('updateUrl', { downloadInfo, url: result.url }) + options.url = result.url + }) + p.then(() => { + dls[downloadInfo.key] = download(options) + }) + }, + removeTask({ commit, state }, index) { + let info = state.list[index] + if (state.list[index].isDownloading) { + dls[info.key].stop().finally(() => { + delete dls[info.key] + }) + } + commit('removeTask', index) + if (dls[info.key]) delete dls[info.key] + }, + resumeTask({ commit, rootState }, downloadInfo) { + let msg = checkPath(rootState.setting.download.savePath) + if (msg) return commit('setStatusText', '检查下载目录出错: ' + msg) + const options = { + url: downloadInfo.url, + path: rootState.setting.download.savePath, + fileName: downloadInfo.fileName, + method: 'get', + override: true, + onEnd() { + commit('onEnd', downloadInfo) + console.log('on complate') + }, + onError(err) { + commit('onError', downloadInfo) + commit('setStatusText', { downloadInfo, text: '链接失效,正在刷新链接' }) + refreshUrl(downloadInfo).then(result => { + commit('updateUrl', { downloadInfo, url: result.url }) + commit('setStatusText', { downloadInfo, text: '链接刷新成功' }) + dls[downloadInfo.key].url = dls[downloadInfo.key].requestURL = result.url + dls[downloadInfo.key].__initProtocol(result.url) + dls[downloadInfo.key].resume() + }).catch(err => { + console.log(err) + }) + console.log(err) + }, + onStateChanged(state) { + console.log(state) + }, + onDownload() { + commit('onDownload', downloadInfo) + console.log('on download') + }, + onProgress(status) { + commit('onProgress', { downloadInfo, status }) + console.log(status) + }, + onPause() { + commit('pauseTask', downloadInfo) + }, + onResume() { + commit('resumeTask', downloadInfo) + }, + } + + let p = options.url ? Promise.resolve() : refreshUrl(downloadInfo).then(result => { + commit('updateUrl', { downloadInfo, url: result.url }) + options.url = result.url + }) + + if (fs.existsSync(downloadInfo.filePath)) { + options.resumeInfo = { + totalFileSize: downloadInfo.progress.total, + } + } + p.then(() => { + dls[downloadInfo.key] = download(options) + }) + }, +} + +// mitations +const mutations = { + addTask(state, downloadInfo) { + state.list.push(downloadInfo) + }, + removeTask(state, index) { + state.list.splice(index, 1) + }, + pauseTask(state, downloadInfo) { + downloadInfo.isDownloading = false + }, + resumeTask(state, downloadInfo) { + downloadInfo.statusText = '恢复下载' + }, + setStatusText(state, { downloadInfo, index, text }) { + if (downloadInfo) { + downloadInfo.statusText = text + } else { + state.list[index].statusText = text + } + }, + onEnd(state, downloadInfo) { + downloadInfo.isComplate = true + downloadInfo.isDownloading = false + downloadInfo.statusText = '下载完成' + }, + onError(state, downloadInfo) { + downloadInfo.isDownloading = false + downloadInfo.statusText = '任务出错' + }, + onDownload(state, downloadInfo) { + downloadInfo.isDownloading = true + downloadInfo.statusText = '正在下载' + }, + onProgress(state, { downloadInfo, status }) { + downloadInfo.progress.progress = status.progress + downloadInfo.progress.downloaded = status.downloaded + downloadInfo.progress.total = status.total + }, + setTotal(state, { order, downloadInfo }) { + downloadInfo.order = order + }, + updateDownloadList(state, list) { + state.list = list + }, + updateUrl(state, { downloadInfo, url }) { + downloadInfo.url = url + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/renderer/store/modules/index.js b/src/renderer/store/modules/index.js new file mode 100644 index 0000000000..3109e2246b --- /dev/null +++ b/src/renderer/store/modules/index.js @@ -0,0 +1,26 @@ +// https://vuex.vuejs.org/en/modules.html + +const requireModule = require.context('./', true, /\.js$/) +const modules = {} + +requireModule.keys().forEach(fileName => { + if (fileName === './index.js') return + const path = fileName.replace(/(\.\/|\.js)/g, '') + + if (/\//.test(path)) { + // Replace ./ and .js + const [moduleName, imported] = path.split('/') + + if (!modules[moduleName]) { + modules[moduleName] = { + namespaced: true, + } + } + + modules[moduleName][imported] = requireModule(fileName).default + } else { + modules[path] = requireModule(fileName).default + } +}) + +export default modules diff --git a/src/renderer/store/modules/leaderboard.js b/src/renderer/store/modules/leaderboard.js new file mode 100644 index 0000000000..a5916d393a --- /dev/null +++ b/src/renderer/store/modules/leaderboard.js @@ -0,0 +1,63 @@ +import music from '../../utils/music' +const sourceList = {} +const sources = [] +for (const source of music.sources) { + const leaderboard = music[source.id].leaderboard + if (!leaderboard) continue + sourceList[source.id] = leaderboard.list + sources.push(source) +} + +// state +const state = { + list: [], + total: 0, + page: 1, + limit: 30, + key: null, +} + +// getters +const getters = { + sourceInfo: () => ({ sources, sourceList }), + list(state) { + return state.list + }, + info(state) { + return { + total: state.total, + limit: state.limit, + page: state.page, + } + }, +} + +// actions +const actions = { + getList({ state, rootState, commit }, page) { + let source = rootState.setting.leaderboard.source + let tabId = rootState.setting.leaderboard.tabId + let key = `${source}${tabId}${page}}` + if (state.list.length && state.key == key) return true + return music[source].leaderboard.getList(tabId, page).then(result => commit('setList', { result, key })) + }, +} + +// mitations +const mutations = { + setList(state, { result, key }) { + state.list = result.list + state.total = result.total + state.limit = result.limit + state.page = result.page + state.key = key + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/renderer/store/modules/list.js b/src/renderer/store/modules/list.js new file mode 100644 index 0000000000..bf26125166 --- /dev/null +++ b/src/renderer/store/modules/list.js @@ -0,0 +1,47 @@ +// state +const state = { + defaultList: { + name: '试听列表', + list: [], + }, + userList: [], +} + +// getters +const getters = { + defaultList: state => state.defaultList || {}, + userList: state => state.userList, +} + +// actions +const actions = { + +} + +// mitations +const mutations = { + initDefaultList(state, data) { + state.defaultList = data + }, + setDefaultList(state, list) { + state.defaultList.list = list + }, + defaultListAdd(state, musicInfo) { + if (state.defaultList.list.some(s => s.songmid === musicInfo.songmid)) return + state.defaultList.list.push(musicInfo) + }, + defaultListRemove(state, index) { + state.defaultList.list.splice(index, 1) + }, + defaultListClear(state) { + state.defaultList.list.length = 0 + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/renderer/store/modules/player.js b/src/renderer/store/modules/player.js new file mode 100644 index 0000000000..c1a51a92a8 --- /dev/null +++ b/src/renderer/store/modules/player.js @@ -0,0 +1,68 @@ +import music from '../../utils/music' + +// state +const state = { + list: [], + listId: null, + playIndex: -1, + changePlay: false, +} + +// getters +const getters = { + list: state => state.list || [], + listId: state => state.listId, + changePlay: satte => satte.changePlay, + playIndex: state => state.playIndex, +} + +// actions +const actions = { + getUrl({ commit, state }, { musicInfo, type }) { + return music[musicInfo.source].getMusicUrl(musicInfo, type).then(result => commit('setUrl', { musicInfo, url: result.url, type })) + }, + getPic({ commit, state }, musicInfo) { + return music[musicInfo.source].getPic(musicInfo).then(url => commit('getPic', { musicInfo, url })) + }, + getLrc({ commit, state }, musicInfo) { + return music[musicInfo.source].getLyric(musicInfo).then(lrc => commit('setLrc', { musicInfo, lrc })) + }, +} + + +// mitations +const mutations = { + setUrl(state, datas) { + datas.musicInfo.typeUrl[datas.type] = datas.url + }, + getPic(state, datas) { + datas.musicInfo.img = datas.url + }, + setLrc(state, datas) { + datas.musicInfo.lyric = datas.lrc + }, + setList(state, { list, listId, index }) { + state.list = list + state.listId = listId + state.playIndex = index + state.changePlay = true + }, + setPlayIndex(state, index) { + state.playIndex = index + state.changePlay = true + }, + fixPlayIndex(state, index) { + state.playIndex = index + }, + resetChangePlay(state) { + state.changePlay = false + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/renderer/store/modules/search.js b/src/renderer/store/modules/search.js new file mode 100644 index 0000000000..114e4956e9 --- /dev/null +++ b/src/renderer/store/modules/search.js @@ -0,0 +1,53 @@ +import music from '../../utils/music' + +// state +const state = { + list: [], + text: '', + page: 1, + limit: 30, + allPage: 1, + total: 0, +} + +// getters +const getters = { + list: state => state.list || [], + limit: state => state.limit, + info: state => ({ page: state.page, text: state.text }), + listInfo: state => ({ allPage: state.allPage, total: state.total }), +} + +// actions +const actions = { + search({ commit, rootState }, { text, page, limit }) { + return music[rootState.setting.sourceId].musicSearch.search(text, page, limit) + .then(data => commit('setList', { list: data.list, allPage: data.allPage, total: data.total, text, page })) + }, +} + +// mitations +const mutations = { + setList(state, datas) { + state.list = datas.list + state.total = datas.total + state.allPage = datas.allPage + state.page = datas.page + state.text = datas.text + }, + clearList(state) { + state.list.length = 0 + state.page = 0 + state.allPage = 0 + state.total = 0 + state.text = '' + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/renderer/store/mutations.js b/src/renderer/store/mutations.js new file mode 100644 index 0000000000..2e813054da --- /dev/null +++ b/src/renderer/store/mutations.js @@ -0,0 +1,25 @@ +export default { + setTheme(state, val) { + state.setting.themeId = val + }, + setSource(state, val) { + state.setting.sourceId = val + }, + setSetting(state, val) { + state.setting = val + }, + setLeaderboard(state, { tabId, source }) { + if (tabId != null) state.setting.leaderboard.tabId = tabId + if (source != null) state.setting.leaderboard.source = source + }, + setNewVersion(state, val) { + val.history.forEach(ver => { + ver.desc = ver.desc.replace(/\n/g, '
') + }) + val.desc = val.desc.replace(/\n/g, '
') + state.version.newVersion = val + }, + setVersionVisible(state, val) { + state.version.showModal = val + }, +} diff --git a/src/renderer/store/state.js b/src/renderer/store/state.js new file mode 100644 index 0000000000..fe9922b884 --- /dev/null +++ b/src/renderer/store/state.js @@ -0,0 +1,56 @@ + +// const isDev = process.env.NODE_ENV === 'development' +import Store from 'electron-store' +import { updateSetting } from '../utils' +import { version } from '../../../package.json' +let electronStore = new Store() +const setting = updateSetting(electronStore.get('setting')) +electronStore.set('setting', setting) + +export default { + themes: [ + { + id: 0, + name: '绿意盎然', + class: 'green', + }, + { + id: 1, + name: '蓝田生玉', + class: 'blue', + }, + { + id: 2, + name: '信口雌黄', + class: 'yellow', + }, + { + id: 3, + name: '橙黄橘绿', + class: 'orange', + }, + { + id: 4, + name: '热情似火', + class: 'red', + }, + { + id: 5, + name: '重斤球紫', + class: 'purple', + }, + { + id: 6, + name: '灰常美丽', + class: 'grey', + }, + ], + version: { + version, + newVersion: null, + showModal: false, + }, + userInfo: null, + setting, + electronStore, +} diff --git a/src/renderer/utils/download/index.js b/src/renderer/utils/download/index.js new file mode 100644 index 0000000000..eb77c2c1de --- /dev/null +++ b/src/renderer/utils/download/index.js @@ -0,0 +1,93 @@ +import { DownloaderHelper } from 'node-downloader-helper' +// import { pauseResumeTimer } from './util' +import { sizeFormate } from '../index' +import { debugDownload } from '../env' + +// these are the default options +// const options = { +// method: 'GET', // Request Method Verb +// // Custom HTTP Header ex: Authorization, User-Agent +// headers: {}, +// fileName: '', // Custom filename when saved +// override: false, // if true it will override the file, otherwise will append '(number)' to the end of file +// forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it +// // httpRequestOptions: {}, // Override the http request options +// // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs +// } + +export default ({ + url, + path, + fileName, + method = 'get', + headers, + override, + forceResume, + // resumeTime = 5000, + onEnd = () => {}, + onError = () => {}, + onStateChanged = () => {}, + onDownload = () => {}, + onPause = () => {}, + onResume = () => {}, + onProgress = () => {}, + resumeInfo, +} = {}) => { + const dl = new DownloaderHelper(url, path, { + fileName, + method, + headers, + override, + forceResume, + }) + + dl.on('end', () => { + onEnd() + debugDownload && console.log('Download Completed') + }).on('error', err => { + onError(err) + dl.resume() + console.log('Download failed, Attempting Retry') + debugDownload && console.error('Something happend', err) + }).on('stateChanged', state => { + onStateChanged(state) + debugDownload && console.log('State: ', state) + }).on('download', () => { + onDownload() + // pauseResumeTimer(dl, resumeTime) + }).on('progress', stats => { + const progress = stats.progress.toFixed(2) + const speed = sizeFormate(stats.speed) + onProgress({ + progress, + speed, + downloaded: stats.downloaded, + total: stats.total, + }) + if (debugDownload) { + const downloaded = sizeFormate(stats.downloaded) + const total = sizeFormate(stats.total) + console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`) + } + }).on('pause', () => { + onPause() + debugDownload && console.log('paused') + }).on('resume', () => { + onResume() + debugDownload && console.log('resume') + }) + + debugDownload && console.log('Downloading: ', url) + + if (resumeInfo) { + dl.__total = resumeInfo.totalFileSize // <--- Workaround + // dl.__filePath = resumeInfo.filePath // <--- Workaround + dl.__isResumable = true // <--- Workaround + dl.resume() + } else { + dl.start() + } + + return dl +} + diff --git a/src/renderer/utils/download/util.js b/src/renderer/utils/download/util.js new file mode 100644 index 0000000000..bef8abee3b --- /dev/null +++ b/src/renderer/utils/download/util.js @@ -0,0 +1,24 @@ +import { DH_STATES } from 'node-downloader-helper' + + +export const pauseResumeTimer = (_dl, wait) => { + setTimeout(() => { + if (_dl.state === DH_STATES.FINISHED || _dl.state === DH_STATES.FAILED) { + return + } + + _dl + .pause() + .then(() => console.log(`Paused for ${wait / 1000} seconds`)) + .then(() => + setTimeout(() => { + if (!_dl.isResumable()) { + console.warn( + "This URL doesn't support resume, it will start from the beginning" + ) + } + return _dl.resume() + }, wait) + ) + }, wait) +} diff --git a/src/renderer/utils/env.js b/src/renderer/utils/env.js new file mode 100644 index 0000000000..553f01efa5 --- /dev/null +++ b/src/renderer/utils/env.js @@ -0,0 +1,5 @@ +const isDev = process.env.NODE_ENV === 'development' + +export const debug = isDev && true +export const debugRequest = isDev && false +export const debugDownload = isDev && false diff --git a/src/renderer/utils/index.js b/src/renderer/utils/index.js new file mode 100644 index 0000000000..26722fdbe7 --- /dev/null +++ b/src/renderer/utils/index.js @@ -0,0 +1,196 @@ +import fs from 'fs' +import { shell, remote } from 'electron' +import path from 'path' +import os from 'os' + +/** + * 获取两个数之间的随机整数,大于等于min,小于max + * @param {*} min + * @param {*} max + */ +export const getRandom = (min, max) => Math.floor(Math.random() * (max - min)) + min + + +export const sizeFormate = size => { + // https://gist.github.com/thomseddon/3511330 + if (!size) return '0 b' + let units = ['b', 'kB', 'MB', 'GB', 'TB'] + let number = Math.floor(Math.log(size) / Math.log(1024)) + return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}` +} + +export const formatPlayTime = time => { + let m = parseInt(time / 60) + let s = parseInt(time % 60) + return m === 0 && s === 0 ? '--/--' : (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) +} + +export const formatPlayTime2 = time => { + let m = parseInt(time / 60) + let s = parseInt(time % 60) + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) +} + +export const b64DecodeUnicode = str => { + // Going backwards: from bytestream, to percent-encoding, to original string. + return decodeURIComponent(window.atob(str).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }).join('')) +} + +export const decodeName = str => str.replace(/'/g, '\'') + +export const scrollTo = (element, to, duration = 300, fn = function() {}) => { + const start = element.scrollTop || element.scrollY + const change = to - start + const increment = 10 + if (!change) { + fn() + return + } + let currentTime = 0; let val + const easeInOutQuad = (t, b, c, d) => { + t /= d / 2 + if (t < 1) return (c / 2) * t * t + b + t-- + return (-c / 2) * (t * (t - 2) - 1) + b + } + const animateScroll = () => { + currentTime += increment + val = parseInt(easeInOutQuad(currentTime, start, change, duration)) + if (element.scrollTo) { + element.scrollTo(0, val) + } else { + element.scrollTop = val + } + if (currentTime < duration) { + setTimeout(animateScroll, increment) + } else { + fn() + } + } + animateScroll() +} + +/** + * 检查路径是否存在 + * @param {*} path + */ +export const checkPath = path => fs.existsSync(path) + + +/** + * 在资源管理器中打开目录 + * @param {*} 选项 + */ +export const openSelectDir = options => remote.dialog.showOpenDialog(remote.getCurrentWindow(), options) + +/** + * 在资源管理器中打开目录 + * @param {*} 选项 + */ +export const openSaveDir = options => remote.dialog.showSaveDialog(remote.getCurrentWindow(), options) + +/** + * 在资源管理器中打开目录 + * @param {*} dir + */ +export const openDirInExplorer = dir => { + shell.showItemInFolder(dir) +} + +export const checkVersion = (currentVer, targetVer) => { + // console.log(currentVer) + // console.log(targetVer) + currentVer = currentVer.split('.') + targetVer = targetVer.split('.') + let maxLen = Math.max(currentVer.length, targetVer.length) + if (currentVer.length < maxLen) { + for (let index = 0, len = maxLen - currentVer.length; index < len; index++) { + currentVer.push(0) + } + } + if (targetVer.length < maxLen) { + for (let index = 0, len = maxLen - targetVer.length; index < len; index++) { + targetVer.push(0) + } + } + for (let index = 0; index < currentVer.length; index++) { + if (parseInt(currentVer[index]) < parseInt(targetVer[index])) return true + if (parseInt(currentVer[index]) > parseInt(targetVer[index])) return false + } + return false +} + +export const isObject = item => item && typeof item === 'object' && !Array.isArray(item) + +/** + * 对象深度合并 + * 注意:循环引用的对象会出现死循环 + * @param {} target 要合并源对象 + * @param {} source 要合并目标对象 + */ +export const objectDeepMerge = (target, source) => { + let base = {} + Object.keys(source).forEach(item => { + if (Array.isArray(source[item])) { + let arr = Array.isArray(target[item]) ? target[item] : [] + target[item] = arr.concat(source[item]) + return + } else if (isObject(source[item])) { + if (!isObject(target[item])) target[item] = {} + objectDeepMerge(target[item], source[item]) + return + } + base[item] = source[item] + }) + Object.assign(target, base) +} + +/** + * 判断是否父子元素 + * @param {*} parent + * @param {*} children + */ +export const isChildren = (parent, children) => { + return children.parentNode ? children.parentNode === parent ? true : isChildren(parent, children.parentNode) : false +} + +export const updateSetting = setting => { + const defaultVersion = '1.0.1' + const defaultSetting = { + version: defaultVersion, + player: { + togglePlayMethod: 'listLoop', + highQuality: false, + }, + list: { + isShowAlbumName: true, + }, + download: { + savePath: path.join(os.homedir(), 'Desktop'), + fileName: '歌名 - 歌手', + }, + leaderboard: { + source: 'kw', + tabId: 'kwbiaosb', + }, + themeId: 0, + sourceId: 'kw', + randomAnimate: true, + } + const overwriteSetting = { + version: defaultVersion, + sourceId: 'kw', + } + + + if (!setting) { + setting = defaultSetting + } else if (checkVersion(setting.version, defaultSetting.version)) { + objectDeepMerge(defaultSetting, setting) + objectDeepMerge(defaultSetting, overwriteSetting) + setting = defaultSetting + } + return setting +} diff --git a/src/renderer/utils/music/index.js b/src/renderer/utils/music/index.js new file mode 100644 index 0000000000..ab336bbec0 --- /dev/null +++ b/src/renderer/utils/music/index.js @@ -0,0 +1,28 @@ +import kw from './kw' +import kg from './kg' +import tx from './tx' +import wy from './wy' +export default { + sources: [ + { + name: '酷我音乐', + id: 'kw', + }, + { + name: '酷狗音乐', + id: 'kg', + }, + { + name: 'QQ音乐', + id: 'tx', + }, + { + name: '网易音乐', + id: 'wy', + }, + ], + kw, + kg, + tx, + wy, +} diff --git a/src/renderer/utils/music/kg/index.js b/src/renderer/utils/music/kg/index.js new file mode 100644 index 0000000000..ac9a269ab2 --- /dev/null +++ b/src/renderer/utils/music/kg/index.js @@ -0,0 +1,7 @@ +import leaderboard from './leaderboard' + +const kg = { + leaderboard, +} + +export default kg diff --git a/src/renderer/utils/music/kg/leaderboard.js b/src/renderer/utils/music/kg/leaderboard.js new file mode 100644 index 0000000000..748967d715 --- /dev/null +++ b/src/renderer/utils/music/kg/leaderboard.js @@ -0,0 +1,125 @@ +import { httpGet, cancelHttp } from '../../request' +import { formatPlayTime } from '../../index' + +export default { + list: [ + { + id: 'kgtop500', + name: '酷狗TOP500', + bangid: '8888', + }, + { + id: 'kgwlhgb', + name: '网络红歌榜', + bangid: '23784', + }, + { + id: 'kgbsb', + name: '飙升榜', + bangid: '6666', + }, + { + id: 'kgfxb', + name: '分享榜', + bangid: '21101', + }, + { + id: 'kgcyyb', + name: '纯音乐榜', + bangid: '33164', + }, + { + id: 'kggfjqb', + name: '古风金曲榜', + bangid: '33161', + }, + { + id: 'kgyyjqb', + name: '粤语金曲榜', + bangid: '33165', + }, + { + id: 'kgomjqb', + name: '欧美金曲榜', + bangid: '33166', + }, + // { + // id: 'kgdyrgb', + // name: '电音热歌榜', + // bangid: '33160', + // }, + // { + // id: 'kgjdrgb', + // name: 'DJ热歌榜', + // bangid: '24971', + // }, + // { + // id: 'kghyxgb', + // name: '华语新歌榜', + // bangid: '31308', + // }, + ], + getUrl(p, id) { + return `http://www2.kugou.kugou.com/yueku/v9/rank/home/${p}-${id}.html` + }, + regExps: { + total: /total: '(\d+)',/, + page: /page: '(\d+)',/, + limit: /pagesize: '(\d+)',/, + listData: /global\.features = (\[.+\]);/, + }, + _cancelIndex: null, + _cancelPromiseCancelFn: null, + getData(url) { + if (this._cancelIndex != null) { + cancelHttp(this._cancelIndex) + this._cancelPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._cancelPromiseCancelFn = reject + this._cancelIndex = httpGet(url, (err, resp, body) => { + this._cancelIndex = null + this._cancelPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + filterData(rawList) { + return rawList.map(item => ({ + singer: item.singername, + name: item.songname, + albumName: item.album_name, + albumId: item.album_id, + songmid: item.audio_id, + source: 'kg', + interval: formatPlayTime(item.duration / 1000), + img: null, + lrc: null, + typeUrl: {}, + })) + }, + getList(id, page) { + let type = this.list.find(s => s.id === id) + if (!type) return Promise.reject() + return this.getData(this.getUrl(page, type.bangid)).then(html => { + let total = html.match(this.regExps.total) + if (total) total = parseInt(RegExp.$1) + page = html.match(this.regExps.page) + if (page) page = parseInt(RegExp.$1) + let limit = html.match(this.regExps.limit) + if (limit) limit = parseInt(RegExp.$1) + let listData = html.match(this.regExps.listData) + if (listData) listData = this.filterData(JSON.parse(RegExp.$1)) + return { + total, + list: listData, + limit, + page, + } + }) + }, +} diff --git a/src/renderer/utils/music/kw/index.js b/src/renderer/utils/music/kw/index.js new file mode 100644 index 0000000000..c46b2e5018 --- /dev/null +++ b/src/renderer/utils/music/kw/index.js @@ -0,0 +1,121 @@ +import { httpGet, cancelHttp } from '../../request' +import tempSearch from './tempSearch' +import musicSearch from './musicSearch' +import { formatSinger } from './util' +import leaderboard from './leaderboard' +import lyric from './lyric' + +const kw = { + _musicInfoIndex: null, + _musicInfoPromiseCancelFn: null, + _musicPicIndex: null, + _musicPicPromiseCancelFn: null, + // context: null, + + + // init(context) { + // if (this.isInited) return + // this.isInited = true + // this.context = context + + // // this.musicSearch.search('我又想你了').then(res => { + // // console.log(res) + // // }) + + // // this.getMusicUrl('62355680', '320k').then(url => { + // // console.log(url) + // // }) + // }, + + tempSearch, + musicSearch, + leaderboard, + getLyric(songInfo) { + // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer + return lyric.getLyric(songInfo.songmid) + }, + handleMusicInfo(songInfo) { + return this.getMusicInfo(songInfo).then(info => { + // console.log(JSON.stringify(info)) + songInfo.name = info.name + songInfo.singer = formatSinger(info.artist) + songInfo.img = info.pic + songInfo.albumName = info.album + return songInfo + // return Object.assign({}, songInfo, { + // name: info.name, + // singer: formatSinger(info.artist), + // img: info.pic, + // albumName: info.album, + // }) + }) + }, + + getMusicUrl(songInfo, type) { + return new Promise((resolve, reject) => { + httpGet(`https://v1.itooi.cn/kuwo/url?id=${songInfo.songmid}&quality=${type.replace(/k$/, '')}&isRedirect=0`, (err, resp, body) => { + if (err) { + console.log(err) + return this.getMusicUrl(songInfo, type) + } + body.code === 200 ? resolve({ type, url: body.data }) : reject(new Error(body.msg)) + }) + }) + }, + + getMusicInfo(songInfo) { + if (this._musicInfoIndex != null) { + cancelHttp(this._musicInfoIndex) + this._musicInfoPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._musicInfoPromiseCancelFn = reject + this._musicInfoIndex = httpGet(`http://www.kuwo.cn/api/www/music/musicInfo?mid=${songInfo.songmid}`, (err, resp, body) => { + this._musicInfoIndex = null + this._musicInfoPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + body.code === 200 ? resolve(body.data) : reject(new Error(body.msg)) + }) + }) + }, + + getMusicUrls(musicInfo, cb) { + let tasks = [] + let songId = musicInfo.songmid + musicInfo.types.forEach(type => { + tasks.push(kw.getMusicUrl(songId, type.type)) + }) + Promise.all(tasks).then(urlInfo => { + let typeUrl = {} + urlInfo.forEach(info => { + typeUrl[info.type] = info.url + }) + cb(typeUrl) + }) + }, + + getPic(songInfo) { + if (this._musicPicIndex != null) { + cancelHttp(this._musicPicIndex) + this._musicPicPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._musicPicPromiseCancelFn = reject + this._musicPicIndex = httpGet(`https://v1.itooi.cn/kuwo/pic?id=${songInfo.songmid}&isRedirect=0`, (err, resp, body) => { + this._musicPicIndex = null + this._musicPicPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + console.log(body) + body.code === 200 ? resolve(body.data) : reject(new Error(body.msg)) + }) + }) + }, +} + +export default kw diff --git a/src/renderer/utils/music/kw/leaderboard.js b/src/renderer/utils/music/kw/leaderboard.js new file mode 100644 index 0000000000..ec376a07f7 --- /dev/null +++ b/src/renderer/utils/music/kw/leaderboard.js @@ -0,0 +1,190 @@ +import { httpGet, cancelHttp } from '../../request' +import { formatPlayTime } from '../../index' + +export default { + list: [ + { + id: 'kwbiaosb', + name: '飙升榜', + bangid: 93, + }, + { + id: 'kwregb', + name: '热歌榜', + bangid: 16, + }, + { + id: 'kwhuiyb', + name: '会员榜', + bangid: 145, + }, + { + id: 'kwdouyb', + name: '抖音榜', + bangid: 158, + }, + { + id: 'kwqsb', + name: '趋势榜', + bangid: 187, + }, + { + id: 'kwhuaijb', + name: '怀旧榜', + bangid: 26, + }, + { + id: 'kwhuayb', + name: '华语榜', + bangid: 104, + }, + { + id: 'kwyueyb', + name: '粤语榜', + bangid: 182, + }, + { + id: 'kwoumb', + name: '欧美榜', + bangid: 22, + }, + { + id: 'kwhanyb', + name: '韩语榜', + bangid: 184, + }, + { + id: 'kwriyb', + name: '日语榜', + bangid: 183, + }, + ], + getUrl: (p, l, id) => `http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${p - 1}&rn=${l}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`, + getUrl2: (p, l, id) => `http://www.kuwo.cn/api/www/bang/bang/musicList?bangId=${id}&pn=${p}&rn=${l}`, + regExps: { + + }, + limit: 30, + _cancelIndex: null, + _cancelPromiseCancelFn: null, + _cancelIndex2: null, + _cancelPromiseCancelFn2: null, + getData(url) { + if (this._cancelIndex != null) { + cancelHttp(this._cancelIndex) + this._cancelPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._cancelPromiseCancelFn = reject + this._cancelIndex = httpGet(url, (err, resp, body) => { + this._cancelIndex = null + this._cancelPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + getData2(url) { + if (this._cancelIndex2 != null) { + cancelHttp(this._cancelIndex2) + this._cancelPromiseCancelFn2(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._cancelPromiseCancelFn2 = reject + this._cancelIndex2 = httpGet(url, (err, resp, body) => { + this._cancelIndex2 = null + this._cancelPromiseCancelFn2 = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + filterData(rawList, rawList2) { + return rawList.map((item, inedx) => { + let formats = item.formats.split('|') + let types = [] + let _types = {} + if (formats.indexOf('MP3128')) { + types.push({ type: '128k', size: null }) + _types['128k'] = { + size: null, + } + } + if (formats.indexOf('MP3192')) { + types.push({ type: '192k', size: null }) + _types['192k'] = { + size: null, + } + } + if (formats.indexOf('MP3H')) { + types.push({ type: '320k', size: null }) + _types['320k'] = { + size: null, + } + } + if (formats.indexOf('AL')) { + types.push({ type: 'ape', size: null }) + _types['ape'] = { + size: null, + } + } + if (formats.indexOf('ALFLAC')) { + types.push({ type: 'flac', size: null }) + _types['flac'] = { + size: null, + } + } + types.reverse() + return { + singer: item.artist, + name: item.name, + albumName: item.album, + albumId: item.albumid, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(rawList2[inedx].duration), + img: item.pic, + lrc: null, + types, + _types, + typeUrl: {}, + } + }) + }, + loadData(p1, p2, page, bangid) { + return Promise.all([p1, p2]).then(([data1, data2]) => { + // console.log(data1, data2) + if (!data1.musiclist.length) { + return this.loadData(this.getData(this.getUrl(page, this.limit, bangid)), + data2.data.musicList.length + ? Promise.resolve(data2) + : this.getData2(this.getUrl2(page, this.limit, bangid)), page, bangid) + } + if (!data2.data.musicList.length) { + return this.loadData(Promise.resolve(data1), this.getData2(this.getUrl2(page, this.limit, bangid)), page, bangid) + } + return Promise.resolve([data1, data2]) + }) + }, + getList(id, page) { + let type = this.list.find(s => s.id === id) + if (!type) return Promise.reject() + return this.loadData(this.getData(this.getUrl(page, this.limit, type.bangid)), this.getData2(this.getUrl2(page, this.limit, type.bangid)), page, type.bangid).then(([data1, data2]) => { + // console.log(data1.musiclist, data2.data) + let total = parseInt(data1.num) + let list = this.filterData(data1.musiclist, data2.data.musicList) + return { + total, + list, + limit: this.limit, + page, + } + }) + }, +} diff --git a/src/renderer/utils/music/kw/lyric.js b/src/renderer/utils/music/kw/lyric.js new file mode 100644 index 0000000000..63ccfa0785 --- /dev/null +++ b/src/renderer/utils/music/kw/lyric.js @@ -0,0 +1,31 @@ +import { httpGet, cancelHttp } from '../../request' + +export default { + _musicLrcIndex: null, + _musicLrcPromiseCancelFn: null, + formatTime(time) { + let m = parseInt(time / 60) + let s = (time % 60).toFixed(2) + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) + }, + getLyric(songId) { + if (this._musicLrcIndex != null) { + cancelHttp(this._musicLrcIndex) + this._musicLrcPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._musicLrcPromiseCancelFn = reject + this._musicLrcIndex = httpGet(`https://v1.itooi.cn/kuwo/lrc?id=${songId}`, (err, resp, body) => { + this._musicLrcIndex = null + this._musicLrcPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + // console.log(body.data) + // console.log(this.transformLrc(body.data)) + resolve(body) + }) + }) + }, +} diff --git a/src/renderer/utils/music/kw/musicSearch.js b/src/renderer/utils/music/kw/musicSearch.js new file mode 100644 index 0000000000..81e7291963 --- /dev/null +++ b/src/renderer/utils/music/kw/musicSearch.js @@ -0,0 +1,144 @@ +// import '../../polyfill/array.find' +// import jshtmlencode from 'js-htmlencode' +import { httpGet, cancelHttp } from '../../request' +import { formatPlayTime, decodeName } from '../../index' +// import { debug } from '../../utils/env' +import { formatSinger } from './util' + +export default { + regExps: { + mInfo: /bitrate:(\d+),format:(\w+),size:([\w.]+)/, + }, + _musicSearchIndex: null, + _musicSearchPromiseCancelFn: null, + limit: 30, + total: 0, + page: 0, + allPage: 1, + // cancelFn: null, + musicSearch(str, page) { + if (this._musicSearchIndex != null) { + cancelHttp(this._musicSearchIndex) + this._musicSearchPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._musicSearchPromiseCancelFn = reject + this._musicSearchIndex = httpGet(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${this.limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`, (err, resp, body) => { + this._musicSearchIndex = null + this._musicSearchPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + // getImg(songId) { + // return httpGet(`http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_${songId}`) + // }, + // getLrc(songId) { + // return httpGet(`http://mobile.kuwo.cn/mpage/html5/songinfoandlrc?mid=${songId}&flag=0`) + // }, + handleResult(rawData) { + const result = [] + for (let i = 0; i < rawData.length; i++) { + const info = rawData[i] + let songId = info.MUSICRID.replace('MUSIC_', '') + // const format = (info.FORMATS || info.formats).split('|') + + if (!info.MINFO) { + console.log('mInfo is undefined') + return null + } + + const types = [] + const _types = {} + + let infoArr = info.MINFO.split(';') + infoArr.forEach(info => { + info = info.match(this.regExps.mInfo) + if (info) { + switch (info[2]) { + case 'flac': + types.push({ type: 'flac', size: info[3] }) + _types.flac = { + size: info[3].toLocaleUpperCase(), + } + break + case 'ape': + types.push({ type: 'ape', size: info[3] }) + _types.ape = { + size: info[3].toLocaleUpperCase(), + } + break + case 'mp3': + switch (info[1]) { + case '320': + types.push({ type: '320k', size: info[3] }) + _types['320k'] = { + size: info[3].toLocaleUpperCase(), + } + break + case '192': + types.push({ type: '192k', size: info[3] }) + _types['192k'] = { + size: info[3].toLocaleUpperCase(), + } + break + case '128': + types.push({ type: '128k', size: info[3] }) + _types['128k'] = { + size: info[3].toLocaleUpperCase(), + } + break + } + break + } + } + }) + types.reverse() + + let interval = parseInt(info.DURATION) + + result.push({ + name: decodeName(info.SONGNAME), + singer: formatSinger(decodeName(info.ARTIST)), + source: 'kw', + // img = (info.album.name === '' || info.album.name === '空') + // ? `http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_160911.jpg` + // : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${info.album.mid}.jpg` + songmid: songId, + albumId: decodeName(info.ALBUMID || ''), + interval: Number.isNaN(interval) ? 0 : formatPlayTime(interval), + albumName: info.ALBUM ? decodeName(info.ALBUM) : '', + lyric: null, + img: null, + types, + _types, + typeUrl: {}, + }) + } + return result + }, + search(str, page = 1, { limit }) { + if (limit != null) this.limit = limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page).then(result => { + if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, { limit }) + let list = this.handleResult(result.abslist) + + if (list == null) return this.search(str, page, { limit }) + + this.total = parseInt(result.TOTAL) + this.page = page + this.allPage = Math.ceil(this.total / this.limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + total: this.total, + }) + }) + }, +} diff --git a/src/renderer/utils/music/kw/tempSearch.js b/src/renderer/utils/music/kw/tempSearch.js new file mode 100644 index 0000000000..1402dc36be --- /dev/null +++ b/src/renderer/utils/music/kw/tempSearch.js @@ -0,0 +1,43 @@ +import { httpGet, cancelHttp } from '../../request' +import { decodeName } from '../../index' + +export default { + regExps: { + relWord: /RELWORD=(.+)/, + }, + _musicTempSearchIndex: null, + _musicTempSearchPromiseCancelFn: null, + tempSearch(str) { + if (this._musicTempSearchIndex != null) { + cancelHttp(this._musicTempSearchIndex) + this._musicTempSearchPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._musicTempSearchPromiseCancelFn = reject + this._musicTempSearchIndex = httpGet(`http://www.kuwo.cn/api/www/search/searchKey?key=${encodeURIComponent(str)}`, (err, resp, body) => { + this._musicTempSearchIndex = null + this._musicTempSearchPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + handleResult(rawData) { + return rawData.map(info => { + let matchResult = info.match(this.regExps.relWord) + return matchResult ? decodeName(matchResult[1]) : '' + }) + }, + cancelTempSearch() { + if (this._musicTempSearchIndex != null) { + cancelHttp(this._musicTempSearchIndex) + this._musicTempSearchPromiseCancelFn(new Error('取消http请求')) + } + }, + search(str) { + return this.tempSearch(str).then(result => this.handleResult(result.data)) + }, +} diff --git a/src/renderer/utils/music/kw/util.js b/src/renderer/utils/music/kw/util.js new file mode 100644 index 0000000000..86eedb1b1a --- /dev/null +++ b/src/renderer/utils/music/kw/util.js @@ -0,0 +1,2 @@ + +export const formatSinger = rawData => rawData.replace(/&/g, '、') diff --git a/src/renderer/utils/music/tx/index.js b/src/renderer/utils/music/tx/index.js new file mode 100644 index 0000000000..fa5e44b027 --- /dev/null +++ b/src/renderer/utils/music/tx/index.js @@ -0,0 +1,7 @@ +import leaderboard from './leaderboard' + +const tx = { + leaderboard, +} + +export default tx diff --git a/src/renderer/utils/music/tx/leaderboard.js b/src/renderer/utils/music/tx/leaderboard.js new file mode 100644 index 0000000000..11865b8f1a --- /dev/null +++ b/src/renderer/utils/music/tx/leaderboard.js @@ -0,0 +1,203 @@ +import { httpGet, cancelHttp } from '../../request' +import { formatPlayTime, sizeFormate } from '../../index' + +export default { + limit: 300, + list: [ + { + id: 'txlxzsb', + name: '流行榜', + bangid: 4, + }, + { + id: 'txrgb', + name: '热歌榜', + bangid: 26, + }, + { + id: 'txwlhgb', + name: '网络榜', + bangid: 28, + }, + { + id: 'txdyb', + name: '抖音榜', + bangid: 60, + }, + { + id: 'txndb', + name: '内地榜', + bangid: 5, + }, + { + id: 'txxgb', + name: '香港榜', + bangid: 59, + }, + { + id: 'txtwb', + name: '台湾榜', + bangid: 61, + }, + { + id: 'txoumb', + name: '欧美榜', + bangid: 3, + }, + { + id: 'txhgb', + name: '韩国榜', + bangid: 16, + }, + { + id: 'txrbb', + name: '日本榜', + bangid: 17, + }, + // { + // id: 'txtybb', + // name: 'YouTube榜', + // bangid: 128, + // }, + ], + getUrl(id, period, limit) { + return `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&platform=yqq.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ + comm: { + cv: 1602, + ct: 20, + }, + toplist: { + module: 'musicToplist.ToplistInfoServer', + method: 'GetDetail', + param: { + topid: id, + num: limit, + period, + }, + }, + }))}` + }, + regExps: { + periodList: //g, + period: /data-listname="(.+?)" data-tid=".*?\/(.+?)" data-date="(.+?)" .+?<\/i>/, + }, + periods: {}, + periodUrl: 'https://c.y.qq.com/node/pc/wk_v15/top.html', + _cancelIndex: null, + _cancelPromiseCancelFn: null, + getData(url) { + if (this._cancelIndex != null) { + cancelHttp(this._cancelIndex) + this._cancelPromiseCancelFn(new Error('取消http请求')) + } + return new Promise((resolve, reject) => { + this._cancelPromiseCancelFn = reject + this._cancelIndex = httpGet(url, (err, resp, body) => { + this._cancelIndex = null + this._cancelPromiseCancelFn = null + if (err) { + console.log(err) + reject(err) + } + resolve(body) + }) + }) + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.name) + }) + return arr.join('、') + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + let types = [] + let _types = {} + if (item.file.size_128mp3 !== 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_ape !== 0) { + let size = sizeFormate(item.file.size_ape) + types.push({ type: 'ape', size }) + _types.ape = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + types.reverse() + return { + singer: this.getSinger(item.singer), + name: item.title, + albumName: item.album.title, + albumId: item.album.mid, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album.mid, + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (item.album.name === '' || item.album.name === '空') + ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, + lrc: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getPeriods(bangid) { + return this.getData(this.periodUrl).then(html => { + let result = html.match(this.regExps.periodList) + if (!result) return Promise.reject() + result.forEach(item => { + let result = item.match(this.regExps.period) + if (!result) return + this.periods[result[2]] = { + name: result[1], + bangid: result[2], + period: result[3], + } + }) + const info = this.periods[bangid] + return info && info.period + }) + }, + getList(id, page) { + let type = this.list.find(s => s.id === id) + if (!type) return Promise.reject() + let info = this.periods[type.bangid] + let p = info ? Promise.resolve(info.period) : this.getPeriods(type.bangid) + return p.then(period => { + return this.getData(this.getUrl(type.bangid, period, this.limit)).then(data => { + // console.log(data) + if (data.code !== 0) return Promise.reject() + return { + total: data.toplist.data.songInfoList.length, + list: this.filterData(data.toplist.data.songInfoList), + limit: this.limit, + page: 1, + } + }) + }) + }, +} diff --git a/src/renderer/utils/music/wy/index.js b/src/renderer/utils/music/wy/index.js new file mode 100644 index 0000000000..3dcb8fa2d0 --- /dev/null +++ b/src/renderer/utils/music/wy/index.js @@ -0,0 +1,7 @@ +import leaderboard from './leaderboard' + +const wy = { + leaderboard, +} + +export default wy diff --git a/src/renderer/utils/music/wy/leaderboard.js b/src/renderer/utils/music/wy/leaderboard.js new file mode 100644 index 0000000000..fd79e71aaa --- /dev/null +++ b/src/renderer/utils/music/wy/leaderboard.js @@ -0,0 +1,123 @@ +import { httpGet, cancelHttp } from '../../request' +import { formatPlayTime } from '../../index' + +export default { + limit: 300, + list: [ + { + id: 'wybsb', + name: '飙升榜', + bangid: '19723756', + }, + { + id: 'wyrgb', + name: '热歌榜', + bangid: '3778678', + }, + { + id: 'wyxgb', + name: '新歌榜', + bangid: '3779629', + }, + { + id: 'wyycb', + name: '原创榜', + bangid: '2884035', + }, + { + id: 'wygdb', + name: '古典榜', + bangid: '71384707', + }, + { + id: 'wydouyb', + name: '抖音榜', + bangid: '2250011882', + }, + { + id: 'wyhyb', + name: '韩语榜', + bangid: '745956260', + }, + { + id: 'wydianyb', + name: '电音榜', + bangid: '1978921795', + }, + { + id: 'wydjb', + name: '电竞榜', + bangid: '2006508653', + }, + { + id: 'wyktvbb', + name: 'KTV唛榜', + bangid: '21845217', + }, + ], + getUrl(id) { + return `https://music.163.com/discover/toplist?id=${id}` + }, + regExps: { + list: /