├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
6 |
7 |
8 |
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 |
2 |
3 |
38 |
39 |
40 |
54 |
55 |
56 |
57 |
58 |
59 |
127 |
128 |
129 |
130 |
211 |
--------------------------------------------------------------------------------
/src/renderer/components/Main/Downloading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
23 |
38 |
39 |
40 |
41 |
42 |
43 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/renderer/components/Main/Finished.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
27 |
28 |
29 |
30 |
31 |
32 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/renderer/components/Main/NewTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
23 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
{{ $t("message.newTask.start") }}
73 |
{{ $t("message.newTask.cancel") }}
74 |
75 |
76 |
77 |
78 |
221 |
222 |
223 |
224 |
225 |
281 |
--------------------------------------------------------------------------------
/src/renderer/components/Main/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
121 |
122 |
123 |
160 |
161 |
162 |
163 |
164 |
165 |
182 |
--------------------------------------------------------------------------------
/src/renderer/components/Main/Task/Task.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ alias }}
8 |
9 |
{{ bytesToString(totalLength, 2) }}
10 |
11 | {{ bytesToString(uploadLength, 2) }}
12 | , {{ bytesToString(uploadSpeed, 1) + 'B/s' }}
13 |
14 |
15 |
16 |
17 |
20 |
21 |
{{ secondsToString(remainingTime) }}
22 |
{{ completedPercentage }}
23 |
24 |
25 |
26 |
{{ bytesToString(downloadSpeed, 1) + 'B/s' }}
27 |
28 |
29 |
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 |
--------------------------------------------------------------------------------