├── .babelrc ├── .electron-vue ├── build.js ├── dev-client.js ├── dev-runner.js ├── webpack.main.config.js ├── webpack.renderer.config.js └── webpack.web.config.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.zh-CN.md ├── appveyor.yml ├── build └── icons │ ├── 256x256.png │ ├── icon.icns │ ├── icon.ico │ └── icon.svg ├── dist ├── electron │ └── .gitkeep └── web │ └── .gitkeep ├── package-lock.json ├── package.json ├── screenshot ├── mac-downloading.png └── mac-downloading.zh-CN.png ├── src ├── index.ejs ├── main │ ├── appdata.js │ ├── index.dev.js │ └── index.js └── renderer │ ├── App.vue │ ├── assets │ ├── .gitkeep │ ├── badge.png │ ├── complete.mp3 │ ├── error.mp3 │ └── logo.png │ ├── components │ ├── Main.vue │ └── Main │ │ ├── Downloading.vue │ │ ├── Finished.vue │ │ ├── NewTask.vue │ │ ├── Settings.vue │ │ └── Task │ │ └── Task.vue │ ├── lang │ ├── en-US.json │ └── zh-CN.json │ ├── main.js │ ├── router │ └── index.js │ ├── styles │ ├── option.css │ └── toolbar.css │ └── utils │ ├── aria2manager.js │ ├── aria2rpc.js │ ├── aria2server.js │ ├── converter.js │ ├── filetypes.js │ └── jsonrpc.js ├── static ├── .gitkeep └── aria2 │ ├── aria2.conf │ ├── darwin │ └── aria2c │ └── win32 │ └── aria2c.exe └── test ├── .eslintrc ├── e2e ├── index.js ├── specs │ └── Launch.spec.js └── utils.js └── unit ├── index.js ├── karma.conf.js └── specs └── LandingPage.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 7 } 8 | }], 9 | "stage-0" 10 | ], 11 | "plugins": ["istanbul"] 12 | }, 13 | "main": { 14 | "presets": [ 15 | ["env", { 16 | "targets": { "node": 7 } 17 | }], 18 | "stage-0" 19 | ] 20 | }, 21 | "renderer": { 22 | "presets": [ 23 | ["env", { 24 | "modules": false 25 | }], 26 | "stage-0" 27 | ] 28 | }, 29 | "web": { 30 | "presets": [ 31 | ["env", { 32 | "modules": false 33 | }], 34 | "stage-0" 35 | ] 36 | } 37 | }, 38 | "plugins": ["transform-runtime"] 39 | } 40 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean () { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build () { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!.gitkeep']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack (config) { 74 | return new Promise((resolve, reject) => { 75 | webpack(config, (err, stats) => { 76 | if (err) reject(err.stack || err) 77 | else if (stats.hasErrors()) { 78 | let err = '' 79 | 80 | stats.toString({ 81 | chunks: false, 82 | colors: true 83 | }) 84 | .split(/\r?\n/) 85 | .forEach(line => { 86 | err += ` ${line}\n` 87 | }) 88 | 89 | reject(err) 90 | } else { 91 | resolve(stats.toString({ 92 | chunks: false, 93 | colors: true 94 | })) 95 | } 96 | }) 97 | }) 98 | } 99 | 100 | function web () { 101 | del.sync(['dist/web/*', '!.gitkeep']) 102 | webpack(webConfig, (err, stats) => { 103 | if (err || stats.hasErrors()) console.log(err) 104 | 105 | console.log(stats.toString({ 106 | chunks: false, 107 | colors: true 108 | })) 109 | 110 | process.exit() 111 | }) 112 | } 113 | 114 | function greeting () { 115 | const cols = process.stdout.columns 116 | let text = '' 117 | 118 | if (cols > 85) text = 'lets-build' 119 | else if (cols > 60) text = 'lets-|build' 120 | else text = false 121 | 122 | if (text && !isCI) { 123 | say(text, { 124 | colors: ['yellow'], 125 | font: 'simple3d', 126 | space: false 127 | }) 128 | } else console.log(chalk.yellow.bold('\n lets-build')) 129 | console.log() 130 | } 131 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.plugin('compilation', compilation => { 52 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.plugin('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | before (app, ctx) { 68 | app.use(hotMiddleware) 69 | ctx.middleware.waitUntilValid(() => { 70 | resolve() 71 | }) 72 | } 73 | } 74 | ) 75 | 76 | server.listen(9080) 77 | }) 78 | } 79 | 80 | function startMain () { 81 | return new Promise((resolve, reject) => { 82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 83 | 84 | const compiler = webpack(mainConfig) 85 | 86 | compiler.plugin('watch-run', (compilation, done) => { 87 | logStats('Main', chalk.white.bold('compiling...')) 88 | hotMiddleware.publish({ action: 'compiling' }) 89 | done() 90 | }) 91 | 92 | compiler.watch({}, (err, stats) => { 93 | if (err) { 94 | console.log(err) 95 | return 96 | } 97 | 98 | logStats('Main', stats) 99 | 100 | if (electronProcess && electronProcess.kill) { 101 | manualRestart = true 102 | process.kill(electronProcess.pid) 103 | electronProcess = null 104 | startElectron() 105 | 106 | setTimeout(() => { 107 | manualRestart = false 108 | }, 5000) 109 | } 110 | 111 | resolve() 112 | }) 113 | }) 114 | } 115 | 116 | function startElectron () { 117 | electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')]) 118 | 119 | electronProcess.stdout.on('data', data => { 120 | electronLog(data, 'blue') 121 | }) 122 | electronProcess.stderr.on('data', data => { 123 | electronLog(data, 'red') 124 | }) 125 | 126 | electronProcess.on('close', () => { 127 | if (!manualRestart) process.exit() 128 | }) 129 | } 130 | 131 | function electronLog (data, color) { 132 | let log = '' 133 | data = data.toString().split(/\r?\n/) 134 | data.forEach(line => { 135 | log += ` ${line}\n` 136 | }) 137 | if (/[0-9A-z]+/.test(log)) { 138 | console.log( 139 | chalk[color].bold('┏ Electron -------------------') + 140 | '\n\n' + 141 | log + 142 | chalk[color].bold('┗ ----------------------------') + 143 | '\n' 144 | ) 145 | } 146 | } 147 | 148 | function greeting () { 149 | const cols = process.stdout.columns 150 | let text = '' 151 | 152 | if (cols > 104) text = 'electron-vue' 153 | else if (cols > 76) text = 'electron-|vue' 154 | else text = false 155 | 156 | if (text) { 157 | say(text, { 158 | colors: ['yellow'], 159 | font: 'simple3d', 160 | space: false 161 | }) 162 | } else console.log(chalk.yellow.bold('\n electron-vue')) 163 | console.log(chalk.blue(' getting ready...') + '\n') 164 | } 165 | 166 | function init () { 167 | greeting() 168 | 169 | Promise.all([startRenderer(), startMain()]) 170 | .then(() => { 171 | startElectron() 172 | }) 173 | .catch(err => { 174 | console.error(err) 175 | }) 176 | } 177 | 178 | init() 179 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main/index.js') 14 | }, 15 | externals: [ 16 | ...Object.keys(dependencies || {}) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js)$/, 22 | enforce: 'pre', 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'eslint-loader', 26 | options: { 27 | formatter: require('eslint-friendly-formatter') 28 | } 29 | } 30 | }, 31 | { 32 | test: /\.js$/, 33 | use: 'babel-loader', 34 | exclude: /node_modules/ 35 | }, 36 | { 37 | test: /\.node$/, 38 | use: 'node-loader' 39 | } 40 | ] 41 | }, 42 | node: { 43 | __dirname: process.env.NODE_ENV !== 'production', 44 | __filename: process.env.NODE_ENV !== 'production' 45 | }, 46 | output: { 47 | filename: '[name].js', 48 | libraryTarget: 'commonjs2', 49 | path: path.join(__dirname, '../dist/electron') 50 | }, 51 | plugins: [ 52 | new webpack.NoEmitOnErrorsPlugin() 53 | ], 54 | resolve: { 55 | extensions: ['.js', '.json', '.node'] 56 | }, 57 | target: 'electron-main' 58 | } 59 | 60 | /** 61 | * Adjust mainConfig for development settings 62 | */ 63 | if (process.env.NODE_ENV !== 'production') { 64 | mainConfig.plugins.push( 65 | new webpack.DefinePlugin({ 66 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 67 | }) 68 | ) 69 | } 70 | 71 | /** 72 | * Adjust mainConfig for production settings 73 | */ 74 | if (process.env.NODE_ENV === 'production') { 75 | mainConfig.plugins.push( 76 | new BabiliWebpackPlugin(), 77 | new webpack.DefinePlugin({ 78 | 'process.env.NODE_ENV': '"production"' 79 | }) 80 | ) 81 | } 82 | 83 | module.exports = mainConfig 84 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | 14 | /** 15 | * List of node_modules to include in webpack bundle 16 | * 17 | * Required for specific packages like Vue UI libraries 18 | * that provide pure *.vue files that need compiling 19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 20 | */ 21 | let whiteListedModules = ['vue'] 22 | 23 | let rendererConfig = { 24 | devtool: '#cheap-module-eval-source-map', 25 | entry: { 26 | renderer: path.join(__dirname, '../src/renderer/main.js') 27 | }, 28 | externals: [ 29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 30 | ], 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.(js|vue)$/, 35 | enforce: 'pre', 36 | exclude: /node_modules/, 37 | use: { 38 | loader: 'eslint-loader', 39 | options: { 40 | formatter: require('eslint-friendly-formatter') 41 | } 42 | } 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: ExtractTextPlugin.extract({ 47 | fallback: 'style-loader', 48 | use: 'css-loader' 49 | }) 50 | }, 51 | { 52 | test: /\.html$/, 53 | use: 'vue-html-loader' 54 | }, 55 | { 56 | test: /\.js$/, 57 | use: 'babel-loader', 58 | exclude: /node_modules/ 59 | }, 60 | { 61 | test: /\.node$/, 62 | use: 'node-loader' 63 | }, 64 | { 65 | test: /\.vue$/, 66 | use: { 67 | loader: 'vue-loader', 68 | options: { 69 | extractCSS: process.env.NODE_ENV === 'production', 70 | loaders: { 71 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 72 | scss: 'vue-style-loader!css-loader!sass-loader' 73 | } 74 | } 75 | } 76 | }, 77 | { 78 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 79 | use: { 80 | loader: 'url-loader', 81 | query: { 82 | limit: 10000, 83 | name: 'imgs/[name]--[folder].[ext]' 84 | } 85 | } 86 | }, 87 | { 88 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 89 | loader: 'url-loader', 90 | options: { 91 | limit: 10000, 92 | name: 'media/[name]--[folder].[ext]' 93 | } 94 | }, 95 | { 96 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 97 | use: { 98 | loader: 'url-loader', 99 | query: { 100 | limit: 10000, 101 | name: 'fonts/[name]--[folder].[ext]' 102 | } 103 | } 104 | } 105 | ] 106 | }, 107 | node: { 108 | __dirname: process.env.NODE_ENV !== 'production', 109 | __filename: process.env.NODE_ENV !== 'production' 110 | }, 111 | plugins: [ 112 | new ExtractTextPlugin('styles.css'), 113 | new HtmlWebpackPlugin({ 114 | filename: 'index.html', 115 | template: path.resolve(__dirname, '../src/index.ejs'), 116 | minify: { 117 | collapseWhitespace: true, 118 | removeAttributeQuotes: true, 119 | removeComments: true 120 | }, 121 | nodeModules: process.env.NODE_ENV !== 'production' 122 | ? path.resolve(__dirname, '../node_modules') 123 | : false 124 | }), 125 | new webpack.HotModuleReplacementPlugin(), 126 | new webpack.NoEmitOnErrorsPlugin() 127 | ], 128 | output: { 129 | filename: '[name].js', 130 | libraryTarget: 'commonjs2', 131 | path: path.join(__dirname, '../dist/electron') 132 | }, 133 | resolve: { 134 | alias: { 135 | '@': path.join(__dirname, '../src/renderer'), 136 | 'vue$': 'vue/dist/vue.esm.js' 137 | }, 138 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 139 | }, 140 | target: 'electron-renderer' 141 | } 142 | 143 | /** 144 | * Adjust rendererConfig for development settings 145 | */ 146 | if (process.env.NODE_ENV !== 'production') { 147 | rendererConfig.plugins.push( 148 | new webpack.DefinePlugin({ 149 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 150 | }) 151 | ) 152 | } 153 | 154 | /** 155 | * Adjust rendererConfig for production settings 156 | */ 157 | if (process.env.NODE_ENV === 'production') { 158 | rendererConfig.devtool = '' 159 | 160 | rendererConfig.plugins.push( 161 | new BabiliWebpackPlugin(), 162 | new CopyWebpackPlugin([ 163 | { 164 | from: path.join(__dirname, '../static'), 165 | to: path.join(__dirname, '../dist/electron/static'), 166 | ignore: ['.*'] 167 | } 168 | ]), 169 | new webpack.DefinePlugin({ 170 | 'process.env.NODE_ENV': '"production"' 171 | }), 172 | new webpack.LoaderOptionsPlugin({ 173 | minimize: true 174 | }) 175 | ) 176 | } 177 | 178 | module.exports = rendererConfig 179 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | 13 | let webConfig = { 14 | devtool: '#cheap-module-eval-source-map', 15 | entry: { 16 | web: path.join(__dirname, '../src/renderer/main.js') 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|vue)$/, 22 | enforce: 'pre', 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'eslint-loader', 26 | options: { 27 | formatter: require('eslint-friendly-formatter') 28 | } 29 | } 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: ExtractTextPlugin.extract({ 34 | fallback: 'style-loader', 35 | use: 'css-loader' 36 | }) 37 | }, 38 | { 39 | test: /\.html$/, 40 | use: 'vue-html-loader' 41 | }, 42 | { 43 | test: /\.js$/, 44 | use: 'babel-loader', 45 | include: [ path.resolve(__dirname, '../src/renderer') ], 46 | exclude: /node_modules/ 47 | }, 48 | { 49 | test: /\.vue$/, 50 | use: { 51 | loader: 'vue-loader', 52 | options: { 53 | extractCSS: true, 54 | loaders: { 55 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 56 | scss: 'vue-style-loader!css-loader!sass-loader' 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | use: { 64 | loader: 'url-loader', 65 | query: { 66 | limit: 10000, 67 | name: 'imgs/[name].[ext]' 68 | } 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | use: { 74 | loader: 'url-loader', 75 | query: { 76 | limit: 10000, 77 | name: 'fonts/[name].[ext]' 78 | } 79 | } 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new ExtractTextPlugin('styles.css'), 85 | new HtmlWebpackPlugin({ 86 | filename: 'index.html', 87 | template: path.resolve(__dirname, '../src/index.ejs'), 88 | minify: { 89 | collapseWhitespace: true, 90 | removeAttributeQuotes: true, 91 | removeComments: true 92 | }, 93 | nodeModules: false 94 | }), 95 | new webpack.DefinePlugin({ 96 | 'process.env.IS_WEB': 'true' 97 | }), 98 | new webpack.HotModuleReplacementPlugin(), 99 | new webpack.NoEmitOnErrorsPlugin() 100 | ], 101 | output: { 102 | filename: '[name].js', 103 | path: path.join(__dirname, '../dist/web') 104 | }, 105 | resolve: { 106 | alias: { 107 | '@': path.join(__dirname, '../src/renderer'), 108 | 'vue$': 'vue/dist/vue.esm.js' 109 | }, 110 | extensions: ['.js', '.vue', '.json', '.css'] 111 | }, 112 | target: 'web' 113 | } 114 | 115 | /** 116 | * Adjust webConfig for production settings 117 | */ 118 | if (process.env.NODE_ENV === 'production') { 119 | webConfig.devtool = '' 120 | 121 | webConfig.plugins.push( 122 | new BabiliWebpackPlugin(), 123 | new CopyWebpackPlugin([ 124 | { 125 | from: path.join(__dirname, '../static'), 126 | to: path.join(__dirname, '../dist/web/static'), 127 | ignore: ['.*'] 128 | } 129 | ]), 130 | new webpack.DefinePlugin({ 131 | 'process.env.NODE_ENV': '"production"' 132 | }), 133 | new webpack.LoaderOptionsPlugin({ 134 | minimize: true 135 | }) 136 | ) 137 | } 138 | 139 | module.exports = webConfig 140 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/unit/coverage/** 2 | test/unit/*.js 3 | test/e2e/*.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | extends: 'standard', 12 | globals: { 13 | __static: true 14 | }, 15 | plugins: [ 16 | 'html' 17 | ], 18 | 'rules': { 19 | // allow paren-less arrow functions 20 | 'arrow-parens': 0, 21 | // allow async-await 22 | 'generator-star-spacing': 0, 23 | // allow debugger during development 24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/web/* 4 | build/* 5 | !build/icons 6 | coverage 7 | node_modules/ 8 | npm-debug.log 9 | npm-debug.log.* 10 | thumbs.db 11 | !.gitkeep 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | osx_image: xcode8.3 4 | sudo: required 5 | dist: trusty 6 | language: c 7 | matrix: 8 | include: 9 | - os: osx 10 | - os: linux 11 | env: CC=clang CXX=clang++ npm_config_clang=1 12 | compiler: clang 13 | cache: 14 | directories: 15 | - node_modules 16 | - "$HOME/.electron" 17 | - "$HOME/.cache" 18 | addons: 19 | apt: 20 | packages: 21 | - libgnome-keyring-dev 22 | - icnsutils 23 | #- xvfb 24 | before_install: 25 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 26 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 27 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 28 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 29 | install: 30 | #- export DISPLAY=':99.0' 31 | #- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 32 | - nvm install 7 33 | - curl -o- -L https://yarnpkg.com/install.sh | bash 34 | - source ~/.bashrc 35 | - npm install -g xvfb-maybe 36 | - yarn 37 | script: 38 | #- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js 39 | #- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e 40 | - yarn run build 41 | branches: 42 | only: 43 | - master 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Alan Zhang 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Languages: [English](https://github.com/alanzhangzm/Photon) [中文](https://github.com/alanzhangzm/Photon/blob/master/README.zh-CN.md) 2 | 3 | # Photon 4 | 5 | *Photon* is a lightweight multi-threaded downloader based on [aria2](https://github.com/aria2/aria2). It supports **HTTP/HTTPS**, **Magnet links**, **BitTorrent** and **Metalink**. 6 | 7 | *Photon* is cross platform. It has **macOS** and **Windows** releases now and will have Linux release soon. 8 | 9 | For Web frontend of aria2, please have a look at [*Photon WebUI*](https://github.com/alanzhangzm/Photon-WebUI). 10 | 11 | ## Installation 12 | 13 | Latest releases: https://github.com/alanzhangzm/Photon/releases 14 | 15 | ## Screenshots 16 | 17 | **Mac** 18 | 19 | ![mac-downloading](screenshot/mac-downloading.png) 20 | 21 | 22 | ## Extensions 23 | 24 | Since *Photon* uses aria2 as download core, all of the extensions that support aria2 via RPC are also *Photon* compatible. 25 | 26 | The default RPC configuration for *Photon* and aria2: 27 | - URL: http://127.0.0.1:6800/jsonrpc 28 | - Host: 127.0.0.1 29 | - Port: 6800 30 | 31 | Some popular extensions: 32 | - [BaiduExporter](https://github.com/acgotaku/BaiduExporter) 33 | - [ThunderLixianExporter](https://github.com/binux/ThunderLixianExporter) 34 | - [115](https://github.com/acgotaku/115) 35 | 36 | 37 | ## Development 38 | 39 | ``` bash 40 | # install dependencies 41 | npm install 42 | 43 | # serve with hot reload at localhost:9080 44 | npm run dev 45 | 46 | # build electron application for production 47 | npm run build 48 | 49 | # run unit & end-to-end tests 50 | npm test 51 | 52 | 53 | # lint all JS/Vue component files in `src/` 54 | npm run lint 55 | ``` 56 | 57 | This project was generated with [electron-vue](https://github.com/SimulatedGREG/electron-vue)@[7c4e3e9](https://github.com/SimulatedGREG/electron-vue/tree/7c4e3e90a772bd4c27d2dd4790f61f09bae0fcef) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about the original structure can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html). 58 | 59 | 60 | ## License 61 | [Apache-2.0](https://github.com/alanzhangzm/Photon/blob/master/LICENSE) 62 | 63 | ## Thanks 64 | 65 | [Aaron Tang](http://aaron-tang.com) for advice on UX design. 66 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | Languages: [English](https://github.com/alanzhangzm/Photon) [中文](https://github.com/alanzhangzm/Photon/blob/master/README.zh-CN.md) 2 | 3 | # Photon 4 | 5 | *Photon* 是一款基于 [aria2](https://github.com/aria2/aria2) 的多线程下载软件,支持 **HTTP/HTTPS**,**磁力链**,**BT** 和 **Metalink** 下载。 6 | 7 | *Photon* 现发行于 **macOS** 和 **Windows** 平台,即将发布 Linux 版。 8 | 9 | 如果你想要一个管理 aria2 的网页前端,请尝试 [*Photon WebUI*](https://github.com/alanzhangzm/Photon-WebUI/blob/master/README.zh-CN.md)。 10 | 11 | ## 安装 12 | 13 | 最新稳定版: https://github.com/alanzhangzm/Photon/releases 14 | 15 | ## 截图 16 | 17 | **Mac** 18 | 19 | ![mac-downloading](screenshot/mac-downloading.zh-CN.png) 20 | 21 | 22 | 23 | ## 插件 24 | 25 | 因为 *Photon* 的下载核心是 aria2,所有基于 RPC 协议的 aria2 插件都同样适用于 *Photon*。 26 | 27 | *Photon* 和 aria2 默认的 RPC 配置: 28 | - URL: http://127.0.0.1:6800/jsonrpc 29 | - 主机: 127.0.0.1 30 | - 端口: 6800 31 | 32 | 常用的插件: 33 | - 百度云下载插件:[BaiduExporter](https://github.com/acgotaku/BaiduExporter) 34 | - 迅雷离线下载插件:[ThunderLixianExporter](https://github.com/binux/ThunderLixianExporter) 35 | - 115网盘下载插件:[115](https://github.com/acgotaku/115) 36 | 37 | ## 许可证 38 | [Apache-2.0](https://github.com/alanzhangzm/Photon/blob/master/LICENSE) 39 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2017 10 | platform: 11 | - x64 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 8 x64 24 | - choco install yarn --ignore-dependencies 25 | - git reset --hard HEAD 26 | - yarn 27 | - node --version 28 | 29 | build_script: 30 | #- yarn test 31 | - yarn build 32 | 33 | test: off 34 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/build/icons/icon.ico -------------------------------------------------------------------------------- /build/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/electron/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/dist/electron/.gitkeep -------------------------------------------------------------------------------- /dist/web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/dist/web/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photon", 3 | "version": "0.4.3", 4 | "author": "Alan Zhang ", 5 | "description": "A lightweight downloader based on aria2", 6 | "license": "Apache-2.0", 7 | "main": "./dist/electron/main.js", 8 | "scripts": { 9 | "build": "node .electron-vue/build.js && electron-builder --x64", 10 | "build:mac": "node .electron-vue/build.js && electron-builder --x64 --mac", 11 | "build:win": "node .electron-vue/build.js && electron-builder --x64 --win", 12 | "build:linux": "node .electron-vue/build.js && electron-builder --x64 --linux", 13 | "build:all": "node .electron-vue/build.js && electron-builder --x64 -mw", 14 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 15 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 16 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 17 | "dev": "node .electron-vue/dev-runner.js", 18 | "e2e": "npm run pack && mocha test/e2e", 19 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test", 20 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test", 21 | "pack": "npm run pack:main && npm run pack:renderer", 22 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 23 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 24 | "test": "npm run unit && npm run e2e", 25 | "unit": "karma start test/unit/karma.conf.js", 26 | "postinstall": "npm run lint:fix" 27 | }, 28 | "build": { 29 | "productName": "Photon", 30 | "appId": "org.simulatedgreg.electron-vue", 31 | "asarUnpack": [ 32 | "**/static/aria2${/*}" 33 | ], 34 | "directories": { 35 | "output": "build" 36 | }, 37 | "files": [ 38 | "dist/electron/**/*" 39 | ], 40 | "dmg": { 41 | "contents": [ 42 | { 43 | "x": 410, 44 | "y": 150, 45 | "type": "link", 46 | "path": "/Applications" 47 | }, 48 | { 49 | "x": 130, 50 | "y": 150, 51 | "type": "file" 52 | } 53 | ] 54 | }, 55 | "mac": { 56 | "icon": "build/icons/icon.icns", 57 | "target": "zip" 58 | }, 59 | "win": { 60 | "icon": "build/icons/icon.ico" 61 | }, 62 | "linux": { 63 | "icon": "build/icons", 64 | "target": [ 65 | "deb", 66 | "AppImage" 67 | ], 68 | "depends": [ 69 | "gconf2", 70 | "gconf-service", 71 | "libnotify4", 72 | "libappindicator1", 73 | "libxtst6", 74 | "libnss3", 75 | "aria2" 76 | ] 77 | } 78 | }, 79 | "dependencies": { 80 | "@fortawesome/fontawesome-free-webfonts": "^1.0.9", 81 | "parse-torrent": "^7.0.0", 82 | "vue": "^2.3.3", 83 | "vue-electron": "^1.0.6", 84 | "vue-i18n": "^7.8.0", 85 | "vue-router": "^2.5.3" 86 | }, 87 | "devDependencies": { 88 | "babel-core": "^6.25.0", 89 | "babel-eslint": "^7.2.3", 90 | "babel-loader": "^7.1.1", 91 | "babel-plugin-istanbul": "^4.1.1", 92 | "babel-plugin-transform-runtime": "^6.23.0", 93 | "babel-preset-env": "^1.6.0", 94 | "babel-preset-stage-0": "^6.24.1", 95 | "babel-register": "^6.24.1", 96 | "babili-webpack-plugin": "^0.1.2", 97 | "cfonts": "^1.1.3", 98 | "chai": "^4.0.0", 99 | "chalk": "^2.1.0", 100 | "copy-webpack-plugin": "^4.0.1", 101 | "cross-env": "^5.0.5", 102 | "css-loader": "^0.28.4", 103 | "del": "^3.0.0", 104 | "devtron": "^1.4.0", 105 | "electron": "^1.8.8", 106 | "electron-builder": "^19.19.1", 107 | "electron-debug": "^1.4.0", 108 | "electron-devtools-installer": "^2.2.0", 109 | "eslint": "^4.4.1", 110 | "eslint-config-standard": "^10.2.1", 111 | "eslint-friendly-formatter": "^3.0.0", 112 | "eslint-loader": "^1.9.0", 113 | "eslint-plugin-html": "^3.1.1", 114 | "eslint-plugin-import": "^2.7.0", 115 | "eslint-plugin-node": "^5.1.1", 116 | "eslint-plugin-promise": "^3.5.0", 117 | "eslint-plugin-standard": "^3.0.1", 118 | "extract-text-webpack-plugin": "^3.0.0", 119 | "file-loader": "^0.11.2", 120 | "html-webpack-plugin": "^2.30.1", 121 | "inject-loader": "^3.0.0", 122 | "karma": "^2.0.2", 123 | "karma-chai": "^0.1.0", 124 | "karma-coverage": "^2.0.3", 125 | "karma-electron": "^5.1.1", 126 | "karma-mocha": "^1.2.0", 127 | "karma-sourcemap-loader": "^0.3.7", 128 | "karma-spec-reporter": "^0.0.31", 129 | "karma-webpack": "^2.0.1", 130 | "mocha": "^5.2.0", 131 | "multispinner": "^0.2.1", 132 | "node-loader": "^0.6.0", 133 | "require-dir": "^0.3.0", 134 | "spectron": "^3.7.1", 135 | "style-loader": "^0.18.2", 136 | "url-loader": "^1.0.1", 137 | "vue-html-loader": "^1.2.4", 138 | "vue-loader": "^13.0.5", 139 | "vue-style-loader": "^3.0.1", 140 | "vue-template-compiler": "^2.4.2", 141 | "webpack": "^3.5.2", 142 | "webpack-dev-server": "^2.11.5", 143 | "webpack-hot-middleware": "^2.18.2", 144 | "webpack-merge": "^4.1.0" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /screenshot/mac-downloading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/screenshot/mac-downloading.png -------------------------------------------------------------------------------- /screenshot/mac-downloading.zh-CN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/screenshot/mac-downloading.zh-CN.png -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | photon 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/appdata.js: -------------------------------------------------------------------------------- 1 | import OS from 'os' 2 | import FS from 'fs' 3 | import Path from 'path' 4 | 5 | export default class AppData { 6 | static appName () { 7 | return 'Photon' 8 | } 9 | 10 | static dir () { 11 | const join = Path.join 12 | const platform = OS.platform() 13 | const homedir = OS.homedir() 14 | const appName = AppData.appName() 15 | if (platform === 'darwin') return join(homedir, 'Library', 'Application Support', appName) 16 | else if (platform === 'win32') return join(homedir, 'AppData', 'Roaming', appName) 17 | else return join(homedir, '.config', appName) 18 | } 19 | 20 | static writeData (data) { 21 | let dir = AppData.dir() 22 | const conf = Path.join(dir, 'conf.json') 23 | AppData.makeDir(dir) 24 | try { 25 | FS.writeFileSync(conf, JSON.stringify(data), { 'mode': 0o644 }) 26 | } catch (error) { 27 | console.error(error.message) 28 | } 29 | } 30 | 31 | static readData () { 32 | const conf = Path.join(AppData.dir(), 'conf.json') 33 | try { 34 | let data = FS.readFileSync(conf, 'utf8') 35 | return JSON.parse(data) 36 | } catch (error) { 37 | console.error(error.message) 38 | return '' 39 | } 40 | } 41 | 42 | static makeExecutable (path) { 43 | try { 44 | FS.chmodSync(path, 0o755) 45 | return true 46 | } catch (error) { 47 | console.error(error.message) 48 | return false 49 | } 50 | } 51 | 52 | static touchFile (path) { 53 | try { 54 | FS.statSync(path) 55 | } catch (e) { 56 | try { 57 | FS.writeFileSync(path, '', { 'mode': 0o644 }) 58 | } catch (error) { 59 | console.error(error.message) 60 | } 61 | } 62 | } 63 | 64 | static makeDir (path) { 65 | try { 66 | FS.statSync(path) 67 | } catch (e) { 68 | try { 69 | FS.mkdirSync(path, 0o755) 70 | } catch (error) { 71 | console.error(error.message) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Set environment for development 11 | process.env.NODE_ENV = 'development' 12 | 13 | // Install `electron-debug` with `devtron` 14 | require('electron-debug')({ showDevTools: true }) 15 | 16 | // Install `vue-devtools` 17 | require('electron').app.on('ready', () => { 18 | let installExtension = require('electron-devtools-installer') 19 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 20 | .then(() => {}) 21 | .catch(err => { 22 | console.log('Unable to install `vue-devtools`: \n', err) 23 | }) 24 | }) 25 | 26 | // Require `main` process to boot app 27 | require('./index') 28 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { app, BrowserWindow, Menu, dialog } from 'electron' 4 | 5 | /** 6 | * Set `__static` path to static files in production 7 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 8 | */ 9 | if (process.env.NODE_ENV !== 'development') { 10 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') 11 | } 12 | const windowWidth = process.env.NODE_ENV === 'development' ? 1300 : 900 13 | 14 | let aria2process 15 | let mainWindow 16 | const winURL = process.env.NODE_ENV === 'development' 17 | ? `http://localhost:9080` 18 | : `file://${__dirname}/index.html` 19 | 20 | function createWindow () { 21 | /** 22 | * Initial window options 23 | */ 24 | mainWindow = new BrowserWindow({ 25 | useContentSize: true, 26 | width: windowWidth, 27 | height: 600, 28 | minWidth: 800, 29 | minHeight: 600 30 | }) 31 | 32 | mainWindow.loadURL(winURL) 33 | 34 | mainWindow.on('closed', () => { 35 | mainWindow = null 36 | }) 37 | } 38 | 39 | const menuTemplate = [{ 40 | label: 'Application', 41 | submenu: [{ 42 | label: 'About Application', 43 | selector: 'orderFrontStandardAboutPanel:' 44 | }, 45 | { 46 | type: 'separator' 47 | }, 48 | { 49 | label: 'Quit', 50 | accelerator: 'Command+Q', 51 | click: function () { 52 | app.quit() 53 | } 54 | }] 55 | }, 56 | { 57 | label: 'Edit', 58 | submenu: [{ 59 | label: 'Undo', 60 | accelerator: 'CmdOrCtrl+Z', 61 | selector: 'undo:' 62 | }, 63 | { 64 | label: 'Redo', 65 | accelerator: 'Shift+CmdOrCtrl+Z', 66 | selector: 'redo:' 67 | }, 68 | { 69 | type: 'separator' 70 | }, 71 | { 72 | label: 'Cut', 73 | accelerator: 'CmdOrCtrl+X', 74 | selector: 'cut:' 75 | }, 76 | { 77 | label: 'Copy', 78 | accelerator: 'CmdOrCtrl+C', 79 | selector: 'copy:' 80 | }, 81 | { 82 | label: 'Paste', 83 | accelerator: 'CmdOrCtrl+V', 84 | selector: 'paste:' 85 | }, 86 | { 87 | label: 'Select All', 88 | accelerator: 'CmdOrCtrl+A', 89 | selector: 'selectAll:' 90 | }] 91 | }] 92 | 93 | app.on('ready', () => { 94 | if (!aria2process) aria2process = startAria2() 95 | createWindow() 96 | Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)) 97 | mainWindow.setMenu(null) 98 | }) 99 | 100 | app.on('window-all-closed', () => { 101 | app.setBadgeCount(0) 102 | if (process.platform !== 'darwin') { 103 | app.quit() 104 | } 105 | }) 106 | 107 | app.on('activate', () => { 108 | if (mainWindow === null) { 109 | createWindow() 110 | } 111 | }) 112 | 113 | app.on('will-quit', () => { 114 | if (aria2process) aria2process.kill() 115 | }) 116 | 117 | /** 118 | * Auto Updater 119 | * 120 | * Uncomment the following code below and install `electron-updater` to 121 | * support auto updating. Code Signing with a valid certificate is required. 122 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 123 | */ 124 | 125 | /* 126 | import { autoUpdater } from 'electron-updater' 127 | 128 | autoUpdater.on('update-downloaded', () => { 129 | autoUpdater.quitAndInstall() 130 | }) 131 | 132 | app.on('ready', () => { 133 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 134 | }) 135 | */ 136 | 137 | // aria2 138 | function startAria2 () { 139 | const AppData = require('./appdata').default 140 | const spawn = require('child_process').spawn 141 | const join = require('path').join 142 | const platform = require('os').platform() 143 | const homedir = require('os').homedir() 144 | const datadir = AppData.dir() 145 | const root = join(__static, 'aria2').replace('app.asar', 'app.asar.unpacked') 146 | const aria2c = platform === 'linux' ? 'aria2c' : join(root, platform, 'aria2c') 147 | const conf = join(root, 'aria2.conf') 148 | const session = join(datadir, 'aria2.session') 149 | 150 | if (aria2c !== 'aria2c') AppData.makeExecutable(aria2c) 151 | AppData.makeDir(datadir) 152 | AppData.touchFile(session) 153 | 154 | let options = Object.assign({ 155 | 'input-file': session, 156 | 'save-session': session, 157 | 'dht-file-path': join(datadir, 'dht.dat'), 158 | 'dht-file-path6': join(datadir, 'dht6.dat'), 159 | 'quiet': 'true' 160 | }, AppData.readData() || {}) 161 | if (!options.hasOwnProperty('dir')) options['dir'] = join(homedir, 'Downloads') 162 | 163 | let args = ['--conf-path="' + conf + '"'] 164 | for (let key in options) { 165 | args.push('--' + key + '="' + options[key] + '"') 166 | } 167 | return spawn(aria2c, args, {shell: true}, (error, stdout, stderr) => { 168 | if (error) { 169 | console.error(error.message) 170 | const message = 'conflicts with an existing aria2 instance. Please stop the instance and reopen the app.' 171 | dialog.showErrorBox('Warning', message) 172 | app.quit() 173 | } 174 | }) 175 | } 176 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/src/renderer/assets/.gitkeep -------------------------------------------------------------------------------- /src/renderer/assets/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/src/renderer/assets/badge.png -------------------------------------------------------------------------------- /src/renderer/assets/complete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/src/renderer/assets/complete.mp3 -------------------------------------------------------------------------------- /src/renderer/assets/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/src/renderer/assets/error.mp3 -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/components/Main.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 127 | 128 | 129 | 130 | 211 | -------------------------------------------------------------------------------- /src/renderer/components/Main/Downloading.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/renderer/components/Main/Finished.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/renderer/components/Main/NewTask.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 221 | 222 | 223 | 224 | 225 | 281 | -------------------------------------------------------------------------------- /src/renderer/components/Main/Settings.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 160 | 161 | 162 | 163 | 164 | 165 | 182 | -------------------------------------------------------------------------------- /src/renderer/components/Main/Task/Task.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 93 | 94 | 95 | 96 | 97 | 182 | -------------------------------------------------------------------------------- /src/renderer/lang/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "downloading": "Downloading", 4 | "finished": "Finished", 5 | "settings": "Settings" 6 | }, 7 | "newTask": { 8 | "task": "Task", 9 | "urls": "URLs", 10 | "btMetalink": "BT / Metalink", 11 | "seeding": "Seeding", 12 | "choose": "Choose", 13 | "start": "Start", 14 | "cancel": "Cancel", 15 | "filename": "File Name", 16 | "filetype": "Type", 17 | "size": "Size" 18 | }, 19 | "settings": { 20 | "general": "General", 21 | "profile": "Profile", 22 | "rpc": "RPC", 23 | "host": "Host", 24 | "port": "Port", 25 | "token": "Token", 26 | "encryption": "SSL / TLS", 27 | "status": "Status", 28 | "connected": "Connected", 29 | "disconnected": "Not Connected", 30 | "download": "Download", 31 | "directory": "Directory", 32 | "choose": "Choose", 33 | "maxDownloading": "Max Downloading", 34 | "downloadLimit": "Download Limit", 35 | "uploadLimit": "Upload Limit" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "downloading": "正在下载", 4 | "finished": "已完成", 5 | "settings": "设置" 6 | }, 7 | "newTask": { 8 | "task": "任务", 9 | "urls": "链接", 10 | "btMetalink": "BT / Metalink", 11 | "seeding": "做种", 12 | "choose": "选择文件", 13 | "start": "开始", 14 | "cancel": "取消", 15 | "filename": "文件名", 16 | "filetype": "格式", 17 | "size": "大小" 18 | }, 19 | "settings": { 20 | "general": "通用", 21 | "profile": "配置文件", 22 | "rpc": "RPC", 23 | "host": "主机", 24 | "port": "端口", 25 | "token": "密码", 26 | "encryption": "SSL / TLS", 27 | "status": "状态", 28 | "connected": "已连接", 29 | "disconnected": "未连接", 30 | "download": "下载", 31 | "directory": "文件夹", 32 | "choose": "选择", 33 | "maxDownloading": "最大同时下载", 34 | "downloadLimit": "下载限速", 35 | "uploadLimit": "上传限速" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import Aria2Manager from '@/utils/aria2manager' 5 | 6 | import App from './App' 7 | import router from './router' 8 | 9 | if (!process.env.IS_WEB) Vue.use(require('vue-electron')) 10 | 11 | /* 12 | aria2 13 | */ 14 | let aria2manager = new Aria2Manager() 15 | aria2manager.setSyncInterval(1000) 16 | 17 | /* 18 | Vue 19 | */ 20 | Vue.config.productionTip = false 21 | Vue.use(VueI18n) 22 | 23 | const messages = { 24 | 'en-US': { message: require('@/lang/en-US.json') }, 25 | 'zh-CN': { message: require('@/lang/zh-CN.json') } 26 | } 27 | const i18n = new VueI18n({ 28 | locale: navigator.language, 29 | fallbackLocale: 'en-US', 30 | messages 31 | }) 32 | 33 | new Vue({ 34 | components: { App }, 35 | router, 36 | i18n, 37 | template: '', 38 | data: { 39 | manager: aria2manager 40 | } 41 | }).$mount('#app') 42 | 43 | /* 44 | WebUI 45 | */ 46 | let completeSound = new Audio(require('@/assets/complete.mp3')) 47 | let errorSound = new Audio(require('@/assets/error.mp3')) 48 | aria2manager.onBtDownloadComplete = (tasks, serverName, serverIndex) => completeSound.play() 49 | aria2manager.onDownloadComplete = (tasks, serverName, serverIndex) => { 50 | if (tasks.some(task => !task.isBT)) completeSound.play() 51 | } 52 | aria2manager.onDownloadError = (tasks, serverName, serverIndex) => errorSound.play() 53 | 54 | /* 55 | Electron 56 | */ 57 | const AppData = require('../main/appdata').default 58 | const { app, powerSaveBlocker } = require('electron').remote 59 | const webFrame = require('electron').webFrame 60 | let aria2server = aria2manager.servers[0] 61 | 62 | // disable zooming 63 | webFrame.setZoomLevelLimits(1, 1) 64 | 65 | // set app badge (works for macOS and Unity) 66 | setInterval(() => { 67 | let number = aria2server.tasks.active.length + aria2server.tasks.waiting.length 68 | app.setBadgeCount(number) 69 | }, 1000) 70 | 71 | // prevent suspension when downloading 72 | let blocker 73 | setInterval(() => { 74 | if (aria2server.isDownloading) { 75 | if (blocker === undefined || !powerSaveBlocker.isStarted(blocker)) blocker = powerSaveBlocker.start('prevent-app-suspension') 76 | } else { 77 | if (blocker && powerSaveBlocker.isStarted(blocker)) powerSaveBlocker.stop(blocker) 78 | } 79 | }, 30000) 80 | 81 | app.on('will-quit', () => { 82 | AppData.writeData(aria2server.options) 83 | }) 84 | -------------------------------------------------------------------------------- /src/renderer/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | routes: [{ 8 | path: '/', 9 | name: 'main', 10 | component: require('@/components/Main').default, 11 | children: [ 12 | { 13 | path: 'downloading', 14 | name: 'downloading', 15 | component: require('@/components/Main/Downloading').default 16 | }, 17 | { 18 | path: 'finished', 19 | name: 'finished', 20 | component: require('@/components/Main/Finished').default 21 | }, 22 | { 23 | path: 'settings', 24 | name: 'settings', 25 | component: require('@/components/Main/Settings').default 26 | }, 27 | { 28 | path: 'newTask', 29 | name: 'newTask', 30 | component: require('@/components/Main/NewTask').default 31 | }, 32 | { 33 | path: '*', 34 | redirect: 'downloading' 35 | } 36 | ] 37 | }, 38 | { 39 | path: '*', 40 | redirect: '/' 41 | } 42 | ] 43 | }) 44 | -------------------------------------------------------------------------------- /src/renderer/styles/option.css: -------------------------------------------------------------------------------- 1 | .group { 2 | padding: 8px 0; 3 | border-bottom: 1px solid lightgray; 4 | } 5 | 6 | .header { 7 | padding: 8px 16px; 8 | font-size: 16px; 9 | font-weight: bold; 10 | } 11 | 12 | .row { 13 | padding: 8px 16px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | } 18 | 19 | .row > .left { 20 | flex: 0 0 140px; 21 | padding: 0 8px; 22 | } 23 | 24 | .row > .right { 25 | flex: 1 1 auto; 26 | padding: 0 8px; 27 | } 28 | 29 | .pair { 30 | display: flex; 31 | margin: 0 -4px; 32 | } 33 | 34 | .pair > .fixed { 35 | flex: 0 0 auto; 36 | margin: 0 4px; 37 | } 38 | 39 | .pair > .expanded { 40 | flex: 1 1 auto; 41 | margin: 0 4px; 42 | } 43 | 44 | label { 45 | display: block; 46 | font-size: 16px; 47 | text-align: right; 48 | } 49 | 50 | input, textarea, select { 51 | padding: 8px 12px; 52 | box-sizing: border-box; 53 | border: 1px solid #ccc; 54 | border-radius: 4px; 55 | outline: none; 56 | font-size: 16px; 57 | } 58 | 59 | textarea { 60 | height: 160px; 61 | resize: vertical; 62 | } 63 | 64 | input[type=text], input[type=password], textarea { 65 | width: 100%; 66 | } 67 | 68 | input[type=number], select{ 69 | min-width: 80px; 70 | } 71 | 72 | input:focus, textarea:focus { 73 | border: 1px solid #666; 74 | } 75 | 76 | input:invalid { 77 | border: 1px solid red; 78 | } 79 | 80 | input:disabled { 81 | border: 1px solid #ddd; 82 | color: #444; 83 | opacity: 0.5; 84 | pointer-events: none; 85 | } 86 | 87 | .button { 88 | padding: 8px; 89 | border: 1px solid #ccc; 90 | border-radius: 4px; 91 | background-color: #fafafa; 92 | text-align: center; 93 | font-size: 16px; 94 | } 95 | 96 | .button:hover { 97 | text-decoration: none; 98 | cursor: pointer; 99 | border: 1px solid #666; 100 | } 101 | 102 | .button-large { 103 | min-width: 64px; 104 | padding: 8px 8px; 105 | font-size: 20px; 106 | } 107 | 108 | .vspace { 109 | margin: 12px 0 0 0; 110 | } 111 | 112 | .hspace { 113 | margin: 0 0 0 16px; 114 | } 115 | 116 | .hidden { 117 | display: none; 118 | } 119 | 120 | .disabled { 121 | opacity: 0.5; 122 | cursor: default; 123 | pointer-events: none; 124 | } 125 | -------------------------------------------------------------------------------- /src/renderer/styles/toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | height: 48px; 3 | position: sticky; 4 | top: 0px; 5 | padding: 0px 8px; 6 | border-bottom: 2px solid lightgray; 7 | color: #333; 8 | background-color: white; 9 | font-size: 24px; 10 | display: flex; 11 | align-items: stretch; 12 | } 13 | 14 | .toolbar > a { 15 | flex: 0 0 48px; 16 | line-height: 48px; 17 | text-align: center; 18 | color: #333; 19 | } 20 | 21 | .toolbar > .disabled { 22 | opacity: 0.4; 23 | cursor: default; 24 | pointer-events: none; 25 | } 26 | 27 | .seperator-h { 28 | margin: 12px 12px; 29 | border: 1px dashed #aaa; 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/utils/aria2manager.js: -------------------------------------------------------------------------------- 1 | import Aria2Server from './aria2server' 2 | 3 | export default class Aria2Manager { 4 | constructor () { 5 | this.servers = this._initServers() 6 | this.serverIndex = 0 7 | this.sync = undefined 8 | } 9 | 10 | addServer () { 11 | this.servers.push(new Aria2Server()) 12 | } 13 | 14 | removeServer () { 15 | if (this.servers.length !== 0) this.servers.splice(this.serverIndex, 1) 16 | if (this.serverIndex >= this.servers.length) this.serverIndex = this.servers.length - 1 17 | } 18 | 19 | setServerIndex (index) { 20 | this.serverIndex = Math.min(this.servers.length - 1, Math.max(0, index)) 21 | } 22 | 23 | setSyncInterval (interval = 3000) { 24 | this.sync = setInterval(() => this.syncTasks(), interval) 25 | } 26 | 27 | clearSyncInterval () { 28 | clearInterval(this.sync) 29 | } 30 | 31 | syncTasks () { 32 | let server = this.servers[this.serverIndex] 33 | server.checkConnection() 34 | server.syncDownloading() 35 | server.syncFinished() 36 | } 37 | 38 | writeStorage () { 39 | let data = { 40 | servers: this.servers.map(server => { 41 | return { 42 | name: server.name, 43 | rpc: server.rpc, 44 | options: server.options 45 | } 46 | }) 47 | } 48 | window.localStorage.setItem(this.constructor.name, JSON.stringify(data)) 49 | } 50 | 51 | _readStorage () { 52 | return JSON.parse(window.localStorage.getItem(this.constructor.name)) || {} 53 | } 54 | 55 | _initServers () { 56 | let servers = this._readStorage().servers || [{}] 57 | return servers.map(server => new Aria2Server(server.name, server.rpc, server.options)) 58 | } 59 | } 60 | 61 | ['onDownloadStart', 'onDownloadPause', 'onDownloadStop', 'onDownloadComplete', 'onDownloadError', 'onBtDownloadComplete'].forEach(method => { 62 | Object.defineProperty(Aria2Manager.prototype, method, { 63 | get: function () { 64 | return undefined 65 | }, 66 | set: function (callback) { 67 | this.servers.forEach((server, serverIndex) => { 68 | server[method] = tasks => { 69 | if (typeof callback === 'function') callback(tasks, server.name, serverIndex) 70 | } 71 | }) 72 | } 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/renderer/utils/aria2rpc.js: -------------------------------------------------------------------------------- 1 | import { RPCHTTP, RPCWebSocket } from './jsonrpc' 2 | 3 | const maxTaskNumber = 1000 4 | const taskStatusKeys = ['gid', 'status', 'totalLength', 'completedLength', 'uploadLength', 'downloadSpeed', 'uploadSpeed', 'connections', 'dir', 'files', 'bittorrent', 'errorCallbackCode', 'errorCallbackMessage'] 5 | 6 | export default class Aria2RPC { 7 | constructor (host = '127.0.0.1', port = 6800, token = '', encryption = false) { 8 | this._date = new Date() 9 | this.setRPC(host, port, token, encryption) 10 | } 11 | 12 | setRPC (host = '127.0.0.1', port = 6800, token = '', encryption = false) { 13 | this._token = 'token:' + token 14 | this._address = host + ':' + port + '/jsonrpc' 15 | if (this._rpc) this._rpc.setAddress(this._address, encryption) 16 | else { 17 | try { 18 | this._rpc = new RPCWebSocket(this._address, encryption, 'aria2') 19 | } catch (error) { 20 | console.error(error.message) 21 | console.warn('Fall back to HTTP request.') 22 | this._rpc = new RPCHTTP(this._address, encryption, 'aria2') 23 | } 24 | } 25 | } 26 | 27 | addUri (uris, options = {}, successCallback, errorCallback) { 28 | const method = 'addUri' 29 | if (uris.constructor !== Array) uris = [uris] 30 | let paramsPool = uris.map(uriGroup => [uriGroup.constructor === Array ? uriGroup : [uriGroup], options]) 31 | this._batchRequest(method, paramsPool, successCallback, errorCallback) 32 | } 33 | 34 | addTorrent (torrent, options = {}, successCallback, errorCallback) { 35 | const method = 'addTorrent' 36 | this._request(method, [torrent, [], options], successCallback, errorCallback) 37 | } 38 | 39 | addMetalink (metalink, options = {}, successCallback, errorCallback) { 40 | const method = 'addMetalink' 41 | this._request(method, [metalink, options], successCallback, errorCallback) 42 | } 43 | 44 | tellStatus (gids, successCallback, errorCallback) { 45 | const method = 'tellStatus' 46 | if (gids.constructor !== Array) gids = [gids] 47 | let paramsPool = gids.map(gid => [gid, taskStatusKeys]) 48 | this._batchRequest(method, paramsPool, successCallback, errorCallback) 49 | } 50 | 51 | tellActive (successCallback, errorCallback) { 52 | const method = 'tellActive' 53 | this._request(method, [taskStatusKeys], successCallback, errorCallback) 54 | } 55 | 56 | tellWaiting (successCallback, errorCallback) { 57 | const method = 'tellWaiting' 58 | this._request(method, [0, maxTaskNumber, taskStatusKeys], successCallback, errorCallback) 59 | } 60 | 61 | tellStopped (successCallback, errorCallback) { 62 | const method = 'tellStopped' 63 | this._request(method, [0, maxTaskNumber, taskStatusKeys], successCallback, errorCallback) 64 | } 65 | 66 | changeGlobalOption (options = {}, successCallback, errorCallback) { 67 | const method = 'changeGlobalOption' 68 | this._request(method, [options], successCallback, errorCallback) 69 | } 70 | 71 | _addListener (method, callback) { 72 | let responseHandler = this._responseHandler 73 | if (this._rpc.constructor === RPCWebSocket) { 74 | this._rpc.addListener(method, response => { 75 | responseHandler(method, response, callback) 76 | }) 77 | } 78 | } 79 | 80 | _request (method, params, successCallback, errorCallback) { 81 | let responseHandler = this._responseHandler 82 | let id = method + '.' + this._date.getTime() 83 | this._rpc.request(method, [this._token].concat(params), id, response => { 84 | responseHandler(method, response, successCallback, errorCallback) 85 | }, errorCallback) 86 | } 87 | 88 | _batchRequest (method, paramsPool, successCallback, errorCallback) { 89 | let id = method + '.' + this._date.getTime() 90 | let requests = paramsPool.map(params => { 91 | return { 92 | method: method, 93 | params: [this._token].concat(params), 94 | id: id 95 | } 96 | }) 97 | let responseHandler = this._responseHandler 98 | this._rpc.batchRequest(requests, response => { 99 | responseHandler(method, response, successCallback, errorCallback) 100 | }, errorCallback) 101 | } 102 | 103 | _responseHandler (method, response, successCallback, errorCallback) { 104 | if (response.constructor === Array) { 105 | let errorResults = response.filter(result => result.hasOwnProperty('error')) 106 | errorResults.forEach(result => { 107 | console.warn('[aria2.' + method + ' error]: ' + response.error.code + ' ' + response.error.message) 108 | }) 109 | if (errorResults.length !== 0 && typeof errorCallback === 'function') errorCallback(errorResults) 110 | let successResults = response.filter(result => !result.hasOwnProperty('error')) 111 | .map(result => result.result || result.params) 112 | if (successResults.length !== 0 && typeof successCallback === 'function') successCallback(successResults) 113 | } else { 114 | if (response.hasOwnProperty('error')) { 115 | console.warn('[aria2.' + method + ' error]: ' + response.error.code + ' ' + response.error.message) 116 | if (typeof errorCallback === 'function') errorCallback(response) 117 | } else { 118 | if (typeof successCallback === 'function') successCallback(response.result || response.params) 119 | } 120 | } 121 | } 122 | } 123 | 124 | ['onDownloadStart', 'onDownloadPause', 'onDownloadStop', 'onDownloadComplete', 'onDownloadError', 'onBtDownloadComplete'].forEach(method => { 125 | Object.defineProperty(Aria2RPC.prototype, method, { 126 | get: function () { 127 | return undefined 128 | }, 129 | set: function (callback) { 130 | this._addListener(method, callback) 131 | } 132 | }) 133 | }); 134 | 135 | ['remove', 'pause', 'unpause', 'getUris', 'removeDownloadResult'].forEach(method => { 136 | Object.defineProperty(Aria2RPC.prototype, method, { 137 | value: function (gids, successCallback, errorCallback) { 138 | if (gids.constructor !== Array) gids = [gids] 139 | let paramsPool = gids.map(gid => [gid]) 140 | this._batchRequest(method, paramsPool, successCallback, errorCallback) 141 | } 142 | }) 143 | }); 144 | 145 | ['pauseAll', 'unpauseAll', 'getGlobalOption', 'getGlobalStat', 'purgeDownloadResult', 'getVersion', 'shutdown', 'saveSession'].forEach(method => { 146 | Object.defineProperty(Aria2RPC.prototype, method, { 147 | value: function (successCallback, errorCallback) { 148 | this._request(method, [], successCallback, errorCallback) 149 | } 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /src/renderer/utils/aria2server.js: -------------------------------------------------------------------------------- 1 | import Aria2RPC from './aria2rpc' 2 | 3 | const defaultRPC = { 4 | host: '127.0.0.1', 5 | port: '6800', 6 | token: '', 7 | encryption: false 8 | } 9 | 10 | const defaultOptions = { 11 | 'max-concurrent-downloads': 5, 12 | 'max-overall-download-limit': 0, 13 | 'max-overall-upload-limit': 262144 14 | } 15 | 16 | const defaultSeedingOptions = { 17 | 'seed-time': '43200', 18 | 'seed-ratio': '10' 19 | } 20 | 21 | const defaultNoSeedingOptions = { 22 | 'seed-time': '0', 23 | 'seed-ratio': '0.1' 24 | } 25 | 26 | export default class Aria2Server { 27 | constructor (name = 'Default', rpc = defaultRPC, options = defaultOptions) { 28 | this._handle = new Aria2RPC(rpc.host, rpc.port, rpc.token, rpc.encryption) 29 | 30 | this.name = name 31 | this.rpc = Object.assign({}, rpc) 32 | this.options = Object.assign({}, options) 33 | this.connection = false 34 | this.tasks = { 35 | active: [], 36 | waiting: [], 37 | paused: [], 38 | stopped: [] 39 | } 40 | } 41 | 42 | get isDownloading () { 43 | return this.tasks.active.some(task => task.completedLength !== task.totalLength) 44 | } 45 | 46 | setServer (name = 'Default', rpc = defaultRPC, options = defaultOptions, ignoreDir = true) { 47 | this.name = name.slice() 48 | this.rpc = Object.assign({}, rpc) 49 | let dir = this.options['dir'] 50 | this.options = Object.assign({}, options) 51 | if (ignoreDir) this.options['dir'] = dir 52 | this._handle.setRPC(rpc.host, rpc.port, rpc.token, rpc.encryption) 53 | let strOptions = {} 54 | for (let key in options) strOptions[key] = options[key].toString() 55 | this._handle.changeGlobalOption(strOptions) 56 | } 57 | 58 | checkConnection (successCallback, errorCallback) { 59 | let that = this 60 | this._handle.getVersion(result => { 61 | that.connection = true 62 | if (typeof successCallback === 'function') successCallback(result) 63 | }, error => { 64 | that.connection = false 65 | if (typeof errorCallback === 'function') errorCallback(error) 66 | }) 67 | } 68 | 69 | addTask (task, successCallback, errorCallback) { 70 | let handle = this._handle 71 | let options = task.seeding ? defaultSeedingOptions : defaultNoSeedingOptions 72 | switch (task.type) { 73 | case 'torrent': 74 | if (task.selectfile) { 75 | options['select-file'] = task.selectfile 76 | } 77 | handle.addTorrent(task.file, options, successCallback, errorCallback) 78 | break 79 | case 'metalink': 80 | handle.addMetalink(task.file, options, successCallback, errorCallback) 81 | break 82 | case 'http': 83 | handle.addUri(task.uris, options, successCallback, errorCallback) 84 | break 85 | default: 86 | } 87 | } 88 | 89 | changeTaskStatus (method, gids = [], successCallback, errorCallback) { 90 | if (method === 'unpause') this._handle.unpause(gids, successCallback, errorCallback) 91 | else if (method === 'pause') this._handle.pause(gids, successCallback, errorCallback) 92 | else if (method === 'remove') this._handle.remove(gids, successCallback, errorCallback) 93 | } 94 | 95 | purgeTasks (gids = [], successCallback, errorCallback) { 96 | this._handle.removeDownloadResult(gids, successCallback, errorCallback) 97 | } 98 | 99 | syncDownloading () { 100 | let tasks = this.tasks 101 | this._handle.tellActive(results => { 102 | tasks.active = results.map(result => this._formatTask(result)) 103 | }, e => { 104 | tasks.active = [] 105 | }) 106 | this._handle.tellWaiting(results => { 107 | tasks.waiting = results.filter(result => result.status === 'waiting') 108 | .map(result => this._formatTask(result)) 109 | tasks.paused = results.filter(result => result.status === 'paused') 110 | .map(result => this._formatTask(result)) 111 | }, e => { 112 | tasks.waiting = [] 113 | tasks.paused = [] 114 | }) 115 | } 116 | 117 | syncFinished () { 118 | let tasks = this.tasks 119 | this._handle.tellStopped(results => { 120 | tasks.stopped = results.map(result => this._formatTask(result)) 121 | }, e => { 122 | tasks.stopped = [] 123 | }) 124 | } 125 | 126 | syncOptions () { 127 | let options = this.options 128 | this._handle.getGlobalOption(result => { 129 | options['dir'] = result['dir'] 130 | options['max-concurrent-downloads'] = parseInt(result['max-concurrent-downloads']) 131 | options['max-overall-download-limit'] = parseInt(result['max-overall-download-limit']) 132 | options['max-overall-upload-limit'] = parseInt(result['max-overall-upload-limit']) 133 | }) 134 | } 135 | 136 | _formatTask (task) { 137 | let pathDir = (path) => path.substr(0, path.lastIndexOf('/')) 138 | return { 139 | gid: task.gid, 140 | status: task.status, 141 | isBT: task.hasOwnProperty('bittorrent') && task['bittorrent'].hasOwnProperty('info'), 142 | name: task.hasOwnProperty('bittorrent') && task['bittorrent'].hasOwnProperty('info') ? task['bittorrent']['info']['name'] : task['files'][0]['path'].replace(/^.*[\\/]/, ''), 143 | totalLength: parseInt(task.totalLength), 144 | completedLength: parseInt(task.completedLength), 145 | uploadLength: parseInt(task.uploadLength), 146 | downloadSpeed: parseInt(task.downloadSpeed), 147 | uploadSpeed: parseInt(task.uploadSpeed), 148 | connections: parseInt(task.connections), 149 | dir: task.dir, 150 | path: pathDir(task.files[0].path) === task.dir ? task.files[0].path 151 | : task.files.map(task => pathDir(task.path)) 152 | .reduce((last, cur) => last.length <= cur.length ? last : cur) 153 | } 154 | } 155 | } 156 | 157 | ['onDownloadStart', 'onDownloadPause', 'onDownloadStop', 'onDownloadComplete', 'onDownloadError', 'onBtDownloadComplete'].forEach(method => { 158 | Object.defineProperty(Aria2Server.prototype, method, { 159 | get: function () { 160 | return undefined 161 | }, 162 | set: function (callback) { 163 | let handle = this._handle 164 | let formatTask = this._formatTask 165 | handle[method] = results => { 166 | let gids = results.map(result => result.gid) 167 | handle.tellStatus(gids, tasks => { 168 | if (typeof callback === 'function') callback(tasks.map(task => formatTask(task))) 169 | }) 170 | } 171 | } 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /src/renderer/utils/converter.js: -------------------------------------------------------------------------------- 1 | export default class Converter { 2 | static secondsToString (seconds) { 3 | if (!seconds || seconds === Infinity) return '' 4 | if (typeof (seconds) === 'string') seconds = parseInt(seconds) 5 | if (seconds >= 86400) { 6 | return '> 1 day' 7 | } else { 8 | let hours = Math.floor(seconds / 3600) 9 | seconds %= 3600 10 | let minutes = Math.floor(seconds / 60) 11 | seconds %= 60 12 | seconds = Math.floor(seconds) 13 | return hours + ':' + (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds 14 | } 15 | } 16 | 17 | static bytesToString (bytes, precision = 0) { 18 | if (!bytes) bytes = 0 19 | if (typeof (bytes) === 'string') bytes = parseInt(bytes) 20 | bytes = Math.round(bytes) 21 | let base = Math.pow(10, parseInt(precision)) 22 | if (bytes >= 1073741824) { 23 | return Math.round((bytes / 1073741824) * base) / base + 'G' 24 | } else if (bytes >= 1048576) { 25 | return Math.round((bytes / 1048576) * base) / base + 'M' 26 | } else if (bytes >= 1024) { 27 | return Math.round((bytes / 1024) * base) / base + 'K' 28 | } else { 29 | return Math.round(bytes) + '' 30 | } 31 | } 32 | 33 | static bytesToUnit (bytes) { 34 | if (!bytes) bytes = 0 35 | if (typeof (bytes) === 'string') bytes = parseInt(bytes) 36 | bytes = Math.round(bytes) 37 | if (bytes >= 1073741824) { 38 | return 'G' 39 | } else if (bytes >= 1048576) { 40 | return 'M' 41 | } else if (bytes >= 1024) { 42 | return 'K' 43 | } else { 44 | return '' 45 | } 46 | } 47 | 48 | static stringToBytes (str) { 49 | if (!str) str = '0' 50 | let bytes = parseFloat(str) 51 | if (str.endsWith('G')) { 52 | return Math.round(bytes * 1073741824) 53 | } else if (str.endsWith('M')) { 54 | return Math.round(bytes * 1048576) 55 | } else if (str.endsWith('K')) { 56 | return Math.round(bytes * 1024) 57 | } else { 58 | return Math.round(bytes) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/utils/filetypes.js: -------------------------------------------------------------------------------- 1 | export const imageExtensions = [ 2 | '.ai', 3 | '.bmp', 4 | '.eps', 5 | '.gif', 6 | '.icn', 7 | '.ico', 8 | '.jpeg', 9 | '.jpg', 10 | '.png', 11 | '.psd', 12 | '.raw', 13 | '.sketch', 14 | '.svg', 15 | '.tif', 16 | '.webp', 17 | '.xd' 18 | ] 19 | 20 | export const audioExtensions = [ 21 | '.aac', 22 | '.ape', 23 | '.flac', 24 | '.flav', 25 | '.m4a', 26 | '.mp3', 27 | '.ogg', 28 | '.wav', 29 | '.wma' 30 | ] 31 | 32 | export const videoExtensions = [ 33 | '.avi', 34 | '.m4a', 35 | '.mkv', 36 | '.mov', 37 | '.mp4', 38 | '.mpg', 39 | '.rmvb', 40 | '.vob', 41 | '.wmv' 42 | ] 43 | -------------------------------------------------------------------------------- /src/renderer/utils/jsonrpc.js: -------------------------------------------------------------------------------- 1 | export class RPCHTTP { 2 | constructor (address, encryption = false, namespace) { 3 | this.namespace = namespace 4 | this.setAddress(address, encryption) 5 | } 6 | 7 | setAddress (address, encryption = false) { 8 | this._url = (encryption ? 'https://' : 'http://') + address 9 | } 10 | 11 | request (method, params = [], id, successCallback, errorCallback) { 12 | let data = this._formatData(method, params, id) 13 | this._fetch(this._url, data, successCallback, errorCallback) 14 | } 15 | 16 | batchRequest (requests, successCallback, errorCallback) { 17 | if (requests.constructor !== Array) requests = [requests] 18 | let data = requests.map(request => this._formatData(request.method, request.params, request.id)) 19 | this._fetch(this._url, data, successCallback, errorCallback) 20 | } 21 | 22 | _formatData (method, params = [], id = '') { 23 | return { 24 | jsonrpc: '2.0', 25 | id: id, 26 | method: this.namespace + '.' + method, 27 | params: params.constructor === Array ? params : [params] 28 | } 29 | } 30 | 31 | _fetch (url, data, successCallback, errorCallback) { 32 | fetch(url, { 33 | method: 'POST', 34 | body: JSON.stringify(data) 35 | }).then(response => { 36 | if (!response.ok) throw Error(response.status + ' ' + response.statusText) 37 | return response.json() 38 | }).then(result => { 39 | if (typeof (successCallback) === 'function') successCallback(result) 40 | }).catch(error => { 41 | console.error('[fetch error]: ' + error.message) 42 | if (typeof (errorCallback) === 'function') errorCallback(error) 43 | }) 44 | } 45 | } 46 | 47 | export class RPCWebSocket { 48 | constructor (address, encryption = false, namespace) { 49 | this.namespace = namespace 50 | this._listeners = {} 51 | this.setAddress(address, encryption) 52 | } 53 | 54 | setAddress (address, encryption) { 55 | this._handles = {} 56 | if (typeof WebSocket !== 'function') throw Error('This client does not support WebSocket.') 57 | else { 58 | let url = (encryption ? 'wss://' : 'ws://') + address 59 | try { 60 | this._socket = new WebSocket(url) 61 | let that = this 62 | this._socket.onclose = event => { 63 | setTimeout(() => { 64 | if (that._socket.readyState > 1) that.setAddress(address, encryption) 65 | }, 10000) 66 | } 67 | this._socket.onerror = error => that._onerror(error, that._handles) 68 | this._socket.onmessage = message => that._onmessage(message, that._handles, that._listeners) 69 | } catch (error) { 70 | console.error(error.message) 71 | } 72 | } 73 | } 74 | 75 | addListener (method, callback) { 76 | if (typeof callback === 'function') this._listeners[this.namespace + '.' + method] = callback 77 | } 78 | 79 | removeListener (method) { 80 | delete this._listeners[this.namespace + '.' + method] 81 | } 82 | 83 | request (method, params = [], id, successCallback, errorCallback) { 84 | this._handles[id] = { 85 | success: successCallback, 86 | error: errorCallback 87 | } 88 | let data = this._formatData(method, params, id) 89 | this._send(data) 90 | } 91 | 92 | batchRequest (requests, successCallback, errorCallback) { 93 | if (requests.constructor !== Array) requests = [requests] 94 | requests.forEach(request => { 95 | this._handles[request.id] = { 96 | success: successCallback, 97 | error: errorCallback 98 | } 99 | }) 100 | let data = requests.map(request => this._formatData(request.method, request.params, request.id)) 101 | this._send(data) 102 | } 103 | 104 | _formatData (method, params = [], id = '') { 105 | return { 106 | jsonrpc: '2.0', 107 | id: id, 108 | method: this.namespace + '.' + method, 109 | params: params.constructor === Array ? params : [params] 110 | } 111 | } 112 | 113 | _send (data) { 114 | let that = this 115 | let socket = this._socket 116 | if (socket.readyState > 1) socket.onerror(Error('WebSocket is in state ' + socket.readyState + '.')) 117 | else if (socket.readyState === 0) setTimeout(() => that._send(data), 1000) 118 | else socket.send(JSON.stringify(data)) 119 | } 120 | 121 | _onerror (error, handles) { 122 | if (error.hasOwnProperty('message')) console.error(error.message) 123 | Object.keys(handles).forEach(id => { 124 | if (typeof handles[id].error === 'function') handles[id].error(error) 125 | delete handles[id] 126 | }) 127 | } 128 | 129 | _onmessage (message, handles, listeners) { 130 | let data = JSON.parse(message.data) 131 | if (data.constructor === Array) { 132 | data = data.reduce((last, cur) => { 133 | if (last.hasOwnProperty(cur.id)) last[cur.id].push(cur) 134 | else last[cur.id] = [cur] 135 | return last 136 | }, {}) 137 | for (let id in data) { 138 | if (handles.hasOwnProperty(id)) { 139 | if (typeof handles[id].success === 'function') handles[id].success(data[id]) 140 | delete handles[id] 141 | } 142 | } 143 | } else if (data.hasOwnProperty('id')) { 144 | if (handles.hasOwnProperty(data.id)) { 145 | if (typeof handles[data.id].success === 'function') handles[data.id].success(data) 146 | delete handles[data.id] 147 | } 148 | } else if (data.hasOwnProperty('method')) { 149 | if (listeners.hasOwnProperty(data.method)) { 150 | if (typeof listeners[data.method] === 'function') listeners[data.method](data) 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/static/.gitkeep -------------------------------------------------------------------------------- /static/aria2/aria2.conf: -------------------------------------------------------------------------------- 1 | ### Basic ### 2 | # The directory to store the downloaded file. 3 | dir=${HOME}/Downloads 4 | # Downloads the URIs listed in FILE. 5 | input-file=${HOME}/.aria2/aria2.session 6 | # Save error/unfinished downloads to FILE on exit. 7 | save-session=${HOME}/.aria2/aria2.session 8 | # Save error/unfinished downloads to a file specified by --save-session option every SEC seconds. If 0 is given, file will be saved only when aria2 exits. Default: 0 9 | save-session-interval=60 10 | # Set the maximum number of parallel downloads for every queue item. See also the --split option. Default: 5 11 | max-concurrent-downloads=5 12 | # Continue downloading a partially downloaded file. 13 | continue=true 14 | # Set max overall download speed in bytes/sec. 0 means unrestricted. Default: 0 15 | max-overall-download-limit=0 16 | # Set max download speed per each download in bytes/sec. 0 means unrestricted. Default: 0 17 | # max-download-limit=0 18 | # Make aria2 quiet (no console output). Default: false 19 | quiet=true 20 | 21 | 22 | ### Advanced ### 23 | # Restart download from scratch if the corresponding control file doesn't exist. Default: false 24 | allow-overwrite=true 25 | # If false is given, aria2 aborts download when a piece length is different from one in a control file. If true is given, you can proceed but some download progress will be lost. Default: false 26 | allow-piece-length-change=true 27 | # Always resume download. If true is given, aria2 always tries to resume download and if resume is not possible, aborts download. If false is given, when all given URIs do not support resume or aria2 encounters N URIs which does not support resume, aria2 downloads file from scratch. Default: true 28 | always-resume=true 29 | # Enable asynchronous DNS. Default: true 30 | async-dns=false 31 | # Rename file name if the same file already exists. This option works only in HTTP(S)/FTP download. Default: true 32 | auto-file-renaming=true 33 | # Handle quoted string in Content-Disposition header as UTF-8 instead of ISO-8859-1, for example, the filename parameter, but not the extended version filename. Default: false 34 | content-disposition-default-utf8=true 35 | # Enable disk cache. If SIZE is 0, the disk cache is disabled. This feature caches the downloaded data in memory, which grows to at most SIZE bytes. SIZE can include K or M. Default: 16M 36 | disk-cache=64M 37 | # Specify file allocation method. none doesn't pre-allocate file space. prealloc pre-allocates file space before download begins. This may take some time depending on the size of the file. If you are using newer file systems such as ext4 (with extents support), btrfs, xfs or NTFS(MinGW build only), falloc is your best choice. It allocates large(few GiB) files almost instantly. Don't use falloc with legacy file systems such as ext3 and FAT32 because it takes almost same time as prealloc and it blocks aria2 entirely until allocation finishes. falloc may not be available if your system doesn't have posix_fallocate(3) function. trunc uses ftruncate(2) system call or platform-specific counterpart to truncate a file to a specified length. Possible Values: none, prealloc, trunc, falloc. Default: prealloc 38 | file-allocation=falloc 39 | # No file allocation is made for files whose size is smaller than SIZE. Default: 5M 40 | no-file-allocation-limit=8M 41 | # Set log level to output to console. LEVEL is either debug, info, notice, warn or error. Default: notice 42 | # console-log-level=notice 43 | # Set log level to output. LEVEL is either debug, info, notice, warn or error. Default: debug 44 | # log-level=debug 45 | # The file name of the log file. If - is specified, log is written to stdout. If empty string("") is specified, or this option is omitted, no log is written to disk at all. 46 | # log= 47 | 48 | 49 | ### RPC ### 50 | # Enable JSON-RPC/XML-RPC server. Default: false 51 | enable-rpc=true 52 | # Pause download after added. This option is effective only when --enable-rpc=true is given. Default: false 53 | # pause=false 54 | # Save the uploaded torrent or metalink meta data in the directory specified by --dir option. If false is given to this option, the downloads added will not be saved by --save-session option. Default: true 55 | # rpc-save-upload-metadata=true 56 | # Add Access-Control-Allow-Origin header field with value * to the RPC response. Default: false 57 | rpc-allow-origin-all=true 58 | # Listen incoming JSON-RPC/XML-RPC requests on all network interfaces. If false is given, listen only on local loopback interface. Default: false 59 | rpc-listen-all=false 60 | # Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024 -65535 Default: 6800 61 | # rpc-listen-port=50100 62 | # Set RPC secret authorization token. 63 | # rpc-secret= 64 | # Use the certificate in FILE for RPC server. The certificate must be either in PKCS12 (.p12, .pfx) or in PEM format. When using PEM, you have to specify the private key via --rpc-private-key as well. Use --rpc-secure option to enable encryption. 65 | # rpc-certificate= 66 | # Use the private key in FILE for RPC server. The private key must be decrypted and in PEM format. Use --rpc-secure option to enable encryption. 67 | # rpc-private-key= 68 | # RPC transport will be encrypted by SSL/TLS. The RPC clients must use https scheme to access the server. For WebSocket client, use wss scheme. Use --rpc-certificate and --rpc-private-key options to specify the server certificate and private key. 69 | # rpc-secure=false 70 | 71 | 72 | ### HTTP/FTP/SFTP ### 73 | # The maximum number of connections to one server for each download. Default: 1 74 | max-connection-per-server=8 75 | # aria2 does not split less than 2*SIZE byte range. Possible Values: 1M -1024M. Default: 20M 76 | min-split-size=8M 77 | # Download a file using N connections. The number of connections to the same host is restricted by the --max-connection-per-server option. Default: 5 78 | split=16 79 | # Set user agent for HTTP(S) downloads. Default: aria2/$VERSION, $VERSION is replaced by package version. 80 | user-agent=Transmission/2.94 81 | 82 | 83 | ### BitTorrent ### 84 | # Save meta data as ".torrent" file. Default: false 85 | # bt-save-metadata=false 86 | # Set TCP port number for BitTorrent downloads. Multiple ports can be specified by using ',' and '-'. Default: 6881-6999 87 | listen-port=55001-55099 88 | # Set max overall upload speed in bytes/sec. 0 means unrestricted. Default: 0 89 | max-overall-upload-limit=256K 90 | # Set max upload speed per each torrent in bytes/sec. 0 means unrestricted. Default: 0 91 | # max-upload-limit=0 92 | # Specify share ratio. Seed completed torrents until share ratio reaches RATIO. Specify 0.0 if you intend to do seeding regardless of share ratio. Default: 1.0 93 | seed-ratio=0.1 94 | # Specify seeding time in (fractional) minutes. Specifying --seed-time=0 disables seeding after download completed. 95 | seed-time=0 96 | # Enable Local Peer Discovery. If a private flag is set in a torrent, aria2 doesn't use this feature for that download even if true is given. Default: false 97 | # bt-enable-lpd=false 98 | # Enable IPv4 DHT functionality. It also enables UDP tracker support. If a private flag is set in a torrent, aria2 doesn't use DHT for that download even if true is given. Default: true 99 | enable-dht=true 100 | # Enable IPv6 DHT functionality. If a private flag is set in a torrent, aria2 doesn't use DHT for that download even if true is given. 101 | enable-dht6=true 102 | # Set UDP listening port used by DHT(IPv4, IPv6) and UDP tracker. Default: 6881-6999 103 | dht-listen-port=55001-55099 104 | # Set host and port as an entry point to IPv4 DHT network. 105 | dht-entry-point=dht.transmissionbt.com:6881 106 | # Set host and port as an entry point to IPv6 DHT network. 107 | dht-entry-point6=dht.transmissionbt.com:6881 108 | # Change the IPv4 DHT routing table file to PATH. Default: $HOME/.aria2/dht.dat if present, otherwise $XDG_CACHE_HOME/aria2/dht.dat. 109 | dht-file-path=${HOME}/.aria2/dht.dat 110 | # Change the IPv6 DHT routing table file to PATH. Default: $HOME/.aria2/dht6.dat if present, otherwise $XDG_CACHE_HOME/aria2/dht6.dat. 111 | dht-file-path6=${HOME}/.aria2/dht6.dat 112 | # Enable Peer Exchange extension. If a private flag is set in a torrent, this feature is disabled for that download even if true is given. Default: true 113 | enable-peer-exchange=true 114 | # Specify the prefix of peer ID. Default: A2-$MAJOR-$MINOR-$PATCH-. For instance, aria2 version 1.18.8 has prefix ID A2-1-18-8-. 115 | peer-id-prefix=-TR2940- 116 | # Specify the string used during the bitorrent extended handshake for the peer’s client version. Default: aria2/$MAJOR.$MINOR.$PATCH, $MAJOR, $MINOR and $PATCH are replaced by major, minor and patch version number respectively. For instance, aria2 version 1.18.8 has peer agent aria2/1.18.8. 117 | peer-agent=Transmission/2.94 118 | # Comma separated list of additional BitTorrent tracker's announce URI. Reference: https://github.com/ngosang/trackerslist/ 119 | bt-tracker=udp://tracker.coppersurfer.tk:6969/announce,udp://tracker.leechers-paradise.org:6969/announce,udp://tracker.opentrackr.org:1337/announce,udp://p4p.arenabg.com:1337/announce,udp://9.rarbg.to:2710/announce,udp://9.rarbg.me:2710/announce,udp://tracker.internetwarriors.net:1337/announce,udp://exodus.desync.com:6969/announce,udp://tracker.tiny-vps.com:6969/announce,udp://retracker.lanta-net.ru:2710/announce,udp://open.stealth.si:80/announce,udp://open.demonii.si:1337/announce,udp://tracker.torrent.eu.org:451/announce,udp://tracker.moeking.me:6969/announce,udp://tracker.cyberia.is:6969/announce,udp://denis.stalker.upeer.me:6969/announce,udp://tracker3.itzmx.com:6961/announce,udp://ipv4.tracker.harry.lu:80/announce,udp://retracker.netbynet.ru:2710/announce,udp://explodie.org:6969/announce,udp://zephir.monocul.us:6969/announce,udp://valakas.rollo.dnsabr.com:2710/announce,udp://tracker.zum.bi:6969/announce,udp://tracker.zerobytes.xyz:1337/announce,udp://tracker.yoshi210.com:6969/announce,udp://tracker.uw0.xyz:6969/announce,udp://tracker.nyaa.uk:6969/announce,udp://tracker.lelux.fi:6969/announce,udp://tracker.iamhansen.xyz:2000/announce,udp://tracker.filemail.com:6969/announce,udp://tracker.dler.org:6969/announce,udp://tracker-udp.gbitt.info:80/announce,udp://retracker.sevstar.net:2710/announce,udp://retracker.akado-ural.ru:80/announce,udp://opentracker.i2p.rocks:6969/announce,udp://opentor.org:2710/announce,udp://open.nyap2p.com:6969/announce,udp://chihaya.toss.li:9696/announce,udp://bt2.archive.org:6969/announce,udp://bt1.archive.org:6969/announce,udp://xxxtor.com:2710/announce,udp://tracker4.itzmx.com:2710/announce,udp://tracker2.itzmx.com:6961/announce,udp://tracker.swateam.org.uk:2710/announce,udp://tracker.sbsub.com:2710/announce,udp://tr.bangumi.moe:6969/announce,udp://qg.lorzl.gq:2710/announce,udp://bt2.54new.com:8080/announce,udp://bt.okmp3.ru:2710/announce,https://tracker.nanoha.org:443/announce,https://tracker.parrotlinux.org:443/announce,https://tracker.opentracker.se:443/announce,https://tracker.lelux.fi:443/announce,https://tracker.gbitt.info:443/announce,https://1337.abcvg.info:443/announce 120 | -------------------------------------------------------------------------------- /static/aria2/darwin/aria2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/static/aria2/darwin/aria2c -------------------------------------------------------------------------------- /static/aria2/win32/aria2c.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmzhang8/Photon/7434dfb8226c58749011d2c4343db97b05fd1efe/static/aria2/win32/aria2c.exe -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "__static": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Set BABEL_ENV to use proper env config 4 | process.env.BABEL_ENV = 'test' 5 | 6 | // Enable use of ES6+ on required files 7 | require('babel-register')({ 8 | ignore: /node_modules/ 9 | }) 10 | 11 | // Attach Chai APIs to global scope 12 | const { expect, should, assert } = require('chai') 13 | global.expect = expect 14 | global.should = should 15 | global.assert = assert 16 | 17 | // Require all JS files in `./specs` for Mocha to consume 18 | require('require-dir')('./specs') 19 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import utils from '../utils' 2 | 3 | describe('Launch', function () { 4 | beforeEach(utils.beforeEach) 5 | afterEach(utils.afterEach) 6 | 7 | it('shows the proper application title', function () { 8 | return this.app.client.getTitle() 9 | .then(title => { 10 | expect(title).to.equal('photon') 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import { Application } from 'spectron' 3 | 4 | export default { 5 | afterEach () { 6 | this.timeout(10000) 7 | 8 | if (this.app && this.app.isRunning()) { 9 | return this.app.stop() 10 | } 11 | }, 12 | beforeEach () { 13 | this.timeout(10000) 14 | this.app = new Application({ 15 | path: electron, 16 | args: ['dist/electron/main.js'], 17 | startTimeout: 10000, 18 | waitTimeout: 10000 19 | }) 20 | 21 | return this.app.start() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.devtools = false 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('webpack-merge') 5 | const webpack = require('webpack') 6 | 7 | const baseConfig = require('../../.electron-vue/webpack.renderer.config') 8 | const projectRoot = path.resolve(__dirname, '../../src/renderer') 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = 'test' 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: '#inline-source-map', 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': '"testing"' 18 | }) 19 | ] 20 | }) 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry 24 | delete webpackConfig.externals 25 | delete webpackConfig.output.libraryTarget 26 | 27 | // apply vue option to apply isparta-loader on js 28 | webpackConfig.module.rules 29 | .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' 30 | 31 | module.exports = config => { 32 | config.set({ 33 | browsers: ['visibleElectron'], 34 | client: { 35 | useIframe: false 36 | }, 37 | coverageReporter: { 38 | dir: './coverage', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | customLaunchers: { 45 | 'visibleElectron': { 46 | base: 'Electron', 47 | flags: ['--show'] 48 | } 49 | }, 50 | frameworks: ['mocha', 'chai'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | reporters: ['spec', 'coverage'], 56 | singleRun: true, 57 | webpack: webpackConfig, 58 | webpackMiddleware: { 59 | noInfo: true 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/specs/LandingPage.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LandingPage from '@/components/LandingPage' 3 | 4 | describe('LandingPage.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: h => h(LandingPage) 9 | }).$mount() 10 | 11 | expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!') 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------