Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3aa18e7f72 | |||
| e939f9e5dc | |||
| 53ec0b1dee | |||
| fcfd32a71e | |||
| 2ff56a3770 | |||
| 48768f0658 | |||
| 3e84230b33 | |||
| 5490946267 | |||
| 9461381042 | |||
| 364d7a8f66 | |||
| 7a5a1554ca | |||
| 8410be59b2 | |||
| 795db0b926 | |||
| 079cab8544 | |||
| 7109747e00 | |||
| 9b698f5a0e | |||
| 2b35fb9bc2 | |||
| 2b80127ad0 | |||
| d47fa3d705 | |||
| 555db61ec0 | |||
| 7c2fc774ca | |||
| 8eeab3d3fa | |||
| 7a5b16aecc | |||
| c5f72414e2 | |||
| 4016f5c02e | |||
| af7eb1f359 | |||
| f30ed3c8f7 | |||
| e9e86fbc83 | |||
| d7b985bc3f | |||
| e307240a60 | |||
| 8dd1d84485 | |||
| 3c7b0b26e3 | |||
| 778e092c17 | |||
| 6e462166d1 | |||
| 812b419379 | |||
| 04dcddd3e1 | |||
| 1cfec289b7 | |||
| 2dcfb8c25e | |||
| 482b9312b1 | |||
| dbb1889fc9 | |||
| a6de12c875 | |||
| d14ddef8e8 | |||
| c8698e5b80 | |||
| e8c99caf87 | |||
| f70595915d | |||
| c5962041cb | |||
| 28bbf26334 | |||
| 3d6931f8d8 | |||
| 46d686bee5 | |||
| 9ffaf1116f | |||
| 2d4a05f3b0 | |||
| e04d3a582e | |||
| 943830f2ed | |||
| 137927d44a | |||
| 819f86632e | |||
| 06f881932c | |||
| cb845ea65a | |||
| 60e4907be6 | |||
| bc38978d6a | |||
| 90c3bbff13 | |||
| 3116c53734 | |||
| 78fb6ba455 | |||
| 86304c126d | |||
| 3424424e67 | |||
| 7206f2fc3c | |||
| bd25e566c8 | |||
| f54bc32e90 | |||
| f6dde55234 | |||
| 1303713f5e | |||
| 57a5c0c932 | |||
| 22a41c332f | |||
| 76e7223e85 | |||
| 014619ed46 | |||
| aec3a25e3a | |||
| 02f6ffa3cb | |||
| 5d32fc633c | |||
| cde2832ae3 | |||
| 8656fa0862 | |||
| 0cd1ed1e34 | |||
| d58e485846 | |||
| a6aca44672 | |||
| cc28e4b19b | |||
| 599dec1d6e | |||
| d020359058 | |||
| 04d68e293b | |||
| 6b93a80888 | |||
| c3400d936e | |||
| 871be123d4 | |||
| bed2148ba0 | |||
| 8b97c90e88 | |||
| 832befbafd | |||
| ddbf27e2f9 | |||
| 64b30b6c26 | |||
| 32c8767e65 | |||
| 69391c6de0 | |||
| fbec4e7c4d | |||
| 7640ba583d | |||
| 924b397727 | |||
| 565afdde74 | |||
| 3dfd4ec4da | |||
| 0a21f590ff | |||
| 79bf90bb3c | |||
| f4602b4b68 | |||
| 93a6f1cfbd | |||
| f0d14afb7e | |||
| d9a69ae009 | |||
| 0f5398b106 | |||
| c6eb96547a | |||
| 31ab487d82 | |||
| 0b2e271663 | |||
| a4a4b2321f | |||
| ef372da87b | |||
| 10cab1a9a0 | |||
| a4a651b397 | |||
| b2770795ad | |||
| f4c8ff66d5 | |||
| 1b450c8022 | |||
| e5b8846286 | |||
| 8de8591725 | |||
| 20c30279b0 | |||
| 9000be502b | |||
| cfe66cf337 | |||
| 3557d17bb6 | |||
| 2af891aab8 | |||
| 0d276de39b | |||
| 8a6beda335 | |||
| 68f1cdc4de | |||
| 5290fcfa14 | |||
| d865b630a3 | |||
| 9d95d294cd | |||
| ea46d9b3c6 | |||
| ef2c992af9 | |||
| 64ee097c85 | |||
| 31517f93cb | |||
| 6d54e557b9 | |||
| 3e61adcea3 | |||
| 84d9ced137 | |||
| bc45ed9d5b | |||
| 3d98104dbf | |||
| fb6986ff6e | |||
| b8507570c6 | |||
| cf3ef7606c | |||
| 12a9fa92c1 | |||
| 0fd6617eba | |||
| 7ebbf929d5 | |||
| 14223c2204 | |||
| 2cfb6b1914 | |||
| c38cf80589 | |||
| 3ee98eae1d | |||
| d2cff6356a | |||
| 3ee432d683 | |||
| 7f1822bb7e | |||
| 0223e691ff | |||
| 117dba9f37 | |||
| e5241d09d7 | |||
| c9ea1dece2 | |||
| 60d4108ddc | |||
| ffc8de4766 | |||
| 66f114bf72 | |||
| 44f00483f9 | |||
| 1866e3fd4f | |||
| cfac883cbf | |||
| 1431bab366 | |||
| bb373947ff | |||
| 8f0dc65341 | |||
| 402185e1a2 | |||
| d59b5c9841 | |||
| eb442e4a7a | |||
| e82e567069 | |||
| dc2876098d | |||
| 6287942fbc | |||
| 389dc080b6 | |||
| 45a23d73e6 | |||
| 8303dd305b | |||
| a98292ce1e | |||
| 8df97b8433 | |||
| f29e95c9bc | |||
| 52e045c886 | |||
| 4632c3619a | |||
| 54c48be29b | |||
| 301f1403df | |||
| 88de047778 | |||
| c119de78ce | |||
| 9d381e16da | |||
| e4c6d6a9c0 | |||
| 5bb727cb6f | |||
| 00f3209c68 | |||
| 3f8b0e6f5f | |||
| 0543bc4e2c | |||
| 9ded42f127 | |||
| 834a1ad839 | |||
| 8f830f6a0d | |||
| ee111c92ee | |||
| 74c3a3c696 | |||
| 4d964ae16e | |||
| be2c2e8383 | |||
| 4e6164816f | |||
| fa7daf377f | |||
| afbe364525 | |||
| ece5cea512 | |||
| dc8fa5c647 | |||
| 4fd96e7cae | |||
| 1f22b5efba | |||
| 801db0ed38 | |||
| f72577de65 | |||
| dd21c76dea | |||
| 5a5a932735 | |||
| d63c1d431d | |||
| 1a12256c4c | |||
| 9394a0a79b | |||
| 5e66205298 | |||
| eb796c3c5f | |||
| 9d17b3e9b2 | |||
| 7ee8d4fa0f | |||
| dba4bfb0e7 | |||
| f9755f69cd | |||
| 7a68e1bb82 | |||
| 3b12f3960f | |||
| dbe26dfa98 | |||
| 3be8952cff | |||
| dc5a368e00 | |||
| 11aca7ea0b | |||
| c35af2b109 | |||
| b33b505ccb | |||
| dd22cc0306 | |||
| 206eda08aa | |||
| f6e29700d0 |
@@ -7,8 +7,7 @@ const chalk = require('chalk')
|
||||
const del = require('del')
|
||||
const { spawn } = require('child_process')
|
||||
const webpack = require('webpack')
|
||||
const Multispinner = require('multispinner')
|
||||
|
||||
const Multispinner = require('@motrix/multispinner')
|
||||
|
||||
const mainConfig = require('./webpack.main.config')
|
||||
const rendererConfig = require('./webpack.renderer.config')
|
||||
@@ -28,7 +27,7 @@ if (process.env.BUILD_TARGET === 'clean') {
|
||||
}
|
||||
|
||||
function clean () {
|
||||
del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
|
||||
del.sync(['release/*', '!.gitkeep'])
|
||||
console.log(`\n${doneLog}\n`)
|
||||
process.exit()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const path = require('path')
|
||||
const { dependencies, build } = require('../package.json')
|
||||
const webpack = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
let mainConfig = {
|
||||
entry: {
|
||||
@@ -17,17 +18,6 @@ let mainConfig = {
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
@@ -49,7 +39,10 @@ let mainConfig = {
|
||||
path: path.join(__dirname, '../dist/electron')
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin()
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new ESLintPlugin({
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -9,9 +9,10 @@ const webpack = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
/**
|
||||
* List of node_modules to include in webpack bundle
|
||||
@@ -23,7 +24,6 @@ const { VueLoaderPlugin } = require('vue-loader')
|
||||
let whiteListedModules = ['vue']
|
||||
|
||||
let rendererConfig = {
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
entry: {
|
||||
index: path.join(__dirname, '../src/renderer/pages/index/main.js')
|
||||
},
|
||||
@@ -33,14 +33,10 @@ let rendererConfig = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
loader: 'worker-loader',
|
||||
options: { filename: '[name].js' }
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -52,7 +48,7 @@ let rendererConfig = {
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
prependData: '@import "@/components/Theme/Variables.scss";',
|
||||
additionalData: '@import "@/components/Theme/Variables.scss";',
|
||||
sassOptions: {
|
||||
includePaths:[__dirname, 'src']
|
||||
}
|
||||
@@ -70,7 +66,7 @@ let rendererConfig = {
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
indentedSyntax: true,
|
||||
prependData: '@import "@/components/Theme/Variables.scss";',
|
||||
additionalData: '@import "@/components/Theme/Variables.scss";',
|
||||
sassOptions: {
|
||||
includePaths:[__dirname, 'src']
|
||||
}
|
||||
@@ -93,10 +89,6 @@ let rendererConfig = {
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'vue-html-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
@@ -124,7 +116,7 @@ let rendererConfig = {
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'imgs/[name]--[folder].[ext]'
|
||||
}
|
||||
@@ -142,7 +134,7 @@ let rendererConfig = {
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'fonts/[name]--[folder].[ext]'
|
||||
}
|
||||
@@ -160,45 +152,35 @@ let rendererConfig = {
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true,
|
||||
discardComments: { removeAll: true }
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Motrix',
|
||||
filename: 'index.html',
|
||||
chunks: ['index'],
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
templateParameters(compilation, assets, options) {
|
||||
return {
|
||||
compilation: compilation,
|
||||
webpack: compilation.getStats().toJson(),
|
||||
webpackConfig: compilation.options,
|
||||
htmlWebpackPlugin: {
|
||||
files: assets,
|
||||
options: options
|
||||
},
|
||||
process
|
||||
}
|
||||
},
|
||||
// minify: {
|
||||
// collapseWhitespace: true,
|
||||
// removeAttributeQuotes: true,
|
||||
// removeComments: true
|
||||
// },
|
||||
isBrowser: false,
|
||||
isDev: process.env.NODE_ENV !== 'production',
|
||||
nodeModules: devMode
|
||||
? path.resolve(__dirname, '../node_modules')
|
||||
: false
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin()
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new ESLintPlugin({
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
})
|
||||
],
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.join(__dirname, '../dist/electron')
|
||||
path: path.join(__dirname, '../dist/electron'),
|
||||
globalObject: 'this',
|
||||
publicPath: ''
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -214,7 +196,8 @@ let rendererConfig = {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
extractComments: false,
|
||||
})
|
||||
}),
|
||||
new CssMinimizerPlugin(),
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -223,6 +206,8 @@ let rendererConfig = {
|
||||
* Adjust rendererConfig for development settings
|
||||
*/
|
||||
if (devMode) {
|
||||
rendererConfig.devtool = 'eval-cheap-module-source-map'
|
||||
|
||||
rendererConfig.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
|
||||
@@ -234,16 +219,14 @@ if (devMode) {
|
||||
* Adjust rendererConfig for production settings
|
||||
*/
|
||||
if (!devMode) {
|
||||
rendererConfig.devtool = ''
|
||||
|
||||
rendererConfig.plugins.push(
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../dist/electron/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
]),
|
||||
globOptions: { ignore: [ '.*' ] }
|
||||
}]
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}),
|
||||
|
||||
@@ -9,9 +9,10 @@ const webpack = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
/**
|
||||
* List of node_modules to include in webpack bundle
|
||||
@@ -23,7 +24,6 @@ const { VueLoaderPlugin } = require('vue-loader')
|
||||
let whiteListedModules = ['vue']
|
||||
|
||||
let webConfig = {
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
entry: {
|
||||
index: path.join(__dirname, '../src/renderer/pages/index/main.js')
|
||||
},
|
||||
@@ -33,14 +33,10 @@ let webConfig = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
loader: 'worker-loader',
|
||||
options: { filename: '[name].js' }
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -52,7 +48,7 @@ let webConfig = {
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
prependData: '@import "@/components/Theme/Variables.scss";',
|
||||
additionalData: '@import "@/components/Theme/Variables.scss";',
|
||||
sassOptions: {
|
||||
includePaths:[__dirname, 'src']
|
||||
}
|
||||
@@ -70,7 +66,7 @@ let webConfig = {
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
indentedSyntax: true,
|
||||
prependData: '@import "@/components/Theme/Variables.scss";',
|
||||
additionalData: '@import "@/components/Theme/Variables.scss";',
|
||||
sassOptions: {
|
||||
includePaths:[__dirname, 'src']
|
||||
}
|
||||
@@ -117,7 +113,7 @@ let webConfig = {
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'imgs/[name].[ext]'
|
||||
}
|
||||
@@ -127,7 +123,7 @@ let webConfig = {
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'fonts/[name].[ext]'
|
||||
}
|
||||
@@ -141,34 +137,18 @@ let webConfig = {
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true,
|
||||
discardComments: { removeAll: true }
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Motrix',
|
||||
filename: 'index.html',
|
||||
chunks: ['index'],
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
templateParameters(compilation, assets, options) {
|
||||
return {
|
||||
compilation: compilation,
|
||||
webpack: compilation.getStats().toJson(),
|
||||
webpackConfig: compilation.options,
|
||||
htmlWebpackPlugin: {
|
||||
files: assets,
|
||||
options: options
|
||||
},
|
||||
process
|
||||
}
|
||||
},
|
||||
// minify: {
|
||||
// collapseWhitespace: true,
|
||||
// removeAttributeQuotes: true,
|
||||
// removeComments: true
|
||||
// },
|
||||
isBrowser: true,
|
||||
isDev: process.env.NODE_ENV !== 'production',
|
||||
nodeModules: devMode
|
||||
? path.resolve(__dirname, '../node_modules')
|
||||
: false
|
||||
@@ -177,11 +157,17 @@ let webConfig = {
|
||||
'process.env.IS_WEB': 'true'
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin()
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new ESLintPlugin({
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
})
|
||||
],
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.join(__dirname, '../dist/web')
|
||||
path: path.join(__dirname, '../dist/web'),
|
||||
globalObject: 'this',
|
||||
publicPath: ''
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -197,25 +183,31 @@ let webConfig = {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
extractComments: false,
|
||||
})
|
||||
}),
|
||||
new CssMinimizerPlugin(),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust webConfig for development settings
|
||||
*/
|
||||
if (devMode) {
|
||||
webConfig.devtool = 'eval-cheap-module-source-map'
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust webConfig for production settings
|
||||
*/
|
||||
if (!devMode) {
|
||||
webConfig.devtool = ''
|
||||
|
||||
webConfig.plugins.push(
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../dist/web/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
]),
|
||||
to: path.join(__dirname, '../dist/electron/static'),
|
||||
globOptions: { ignore: [ '.*' ] }
|
||||
}]
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}),
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
release: ${{ secrets.force_release == 'true' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
# macOS notarization API key
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
## 🌍 翻译指南
|
||||
|
||||
首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://electronjs.org/docs/api/locales)
|
||||
首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://www.electronjs.org/docs/api/app#appgetlocale)
|
||||
|
||||
Motrix 的国际化分两部分:
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ Before you start contributing, make sure you already understand [GitHub flow](ht
|
||||
|
||||
## 🌍 Translation Guide
|
||||
|
||||
First you need to determine the English abbreviation of a language as **locale**, such as en-US, this locale value should strictly refer to the [electron's documentation](https://electronjs.org/docs/api/locales).
|
||||
First you need to determine the English abbreviation of a language as **locale**, such as en-US, this locale value should strictly refer to the [electron's documentation](https://www.electronjs.org/docs/api/app#appgetlocale).
|
||||
|
||||
The internationalization of Motrix is divided into two parts:
|
||||
|
||||
|
||||
+52
-16
@@ -1,18 +1,18 @@
|
||||
# Motrix
|
||||
|
||||
<a href="https://motrix.app">
|
||||
<img src="https://cdn.nlark.com/yuque/0/2018/png/129147/1543735425232-a5d2c99f-d788-43e4-9781-558ff6d21027.png" width="256" alt="App Icon" />
|
||||
<img src="./static/512x512.png" width="256" alt="App Icon" />
|
||||
</a>
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
## 一款全能的下载工具
|
||||
|
||||
[](https://github.com/agalwood/Motrix/releases)  [](https://travis-ci.com/agalwood/Motrix) [](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [](https://github.com/agalwood/Motrix/releases) 
|
||||
[](https://github.com/agalwood/Motrix/releases)   
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
我是个兴趣使然的桌面应用开发者🤓,利用搬砖之余开发了 Motrix。
|
||||
|
||||
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链、某盘等资源。它的界面简洁易用,希望大家喜欢 👻。
|
||||
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链等资源。它的界面简洁易用,希望大家喜欢 👻。
|
||||
|
||||
✈️ 去 [官网](https://motrix.app/zh-CN) 逛逛 | 📖 查看 [帮助手册](http://motrix.app/support/issues)
|
||||
|
||||
@@ -36,15 +36,40 @@ scoop install motrix
|
||||
macOS 用户可以使用 `brew cask` 安装 Motrix,感谢 [Mitscherlich](https://github.com/Mitscherlich) 的 [PR](https://github.com/Homebrew/homebrew-cask/pull/59494)。
|
||||
|
||||
```bash
|
||||
brew update && brew cask install motrix
|
||||
brew update && brew install --cask motrix
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
你可以下载 AppImage(适用于所有 Linux 发行版)软件包或 snap 或从源代码构建安装 Motrix。
|
||||
你可以下载 `AppImage` (适用于所有 Linux 发行版)或 `snap` 来安装 Motrix,更多 Linux 安装包格式请查看 [GitHub/release](https://github.com/agalwood/Motrix/releases) 。
|
||||
|
||||
构建请阅读 **编译打包** 部分。
|
||||
如果你想自己通过编译源码来安装,请阅读 **编译打包** 部分。
|
||||
|
||||
#### AppImage
|
||||
最新版的 Motrix AppImage 需要自己手动进执行桌面集成。请查看 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 的文档进行操作。
|
||||
|
||||
> 桌面集成
|
||||
> electron-builder v21 之后,桌面集成不再是 AppImage 文件的一部分。
|
||||
> 推荐使用 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 集成 AppImage。
|
||||
|
||||
Deepin 20 Beta 用户安装 Motrix 失败的问题,请按照以下方法处理:
|
||||
|
||||
打开`终端`,黏贴运行如下命令之后再次安装 Motrix。
|
||||
```bash
|
||||
sudo apt --fix-broken install
|
||||
```
|
||||
|
||||
#### Snap
|
||||
Motrix 已经上架 [Snapcraft](https://snapcraft.io/motrix) ,Ubuntu 用户推荐从 Snap 商店下载。
|
||||
|
||||
v1.5.10 提示
|
||||
|
||||
系统托盘可能无法正常显示指示器,导致退出应用程序不方便。
|
||||
请取消勾选 偏好设置——基本设置——隐藏应用程序菜单(仅限Windows和Linux),点击保存并应用。然后点击 "文件 "菜单中的 "退出",退出应用程序。
|
||||
|
||||
请更新到 v1.5.12 及以上版本,可以使用键盘组合快捷键 <kbd>Ctrl</kbd> + <kbd>q</kbd> 快速退出应用。
|
||||
|
||||
#### AUR
|
||||
对于 Arch Linux 用户,可以使用 [aur](https://aur.archlinux.org/packages/motrix/) 安装 Motrix,感谢维护者 [weearc](https://github.com/weearc)。
|
||||
|
||||
运行以下命令进行安装:
|
||||
@@ -60,7 +85,8 @@ Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能
|
||||
- 🕹 简洁明了的图形操作界面
|
||||
- 🦄 支持BT和磁力链任务
|
||||
- ☑️ 支持选择性下载BT部分文件
|
||||
- 💾 支持下载某盘资源
|
||||
- 📡 每天自动更新 Tracker 服务器列表
|
||||
- 🔌 UPnP & NAT-PMP 端口映射
|
||||
- 🎛 最高支持 10 个任务同时下载
|
||||
- 🚀 单任务最高支持 64 线程下载
|
||||
- 🚥 设置上传/下载限速
|
||||
@@ -68,14 +94,15 @@ Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能
|
||||
- 🔔 下载完成后通知
|
||||
- 💻 支持触控栏快捷键 (Mac 专享)
|
||||
- 🤖 常驻系统托盘,操作更加便捷
|
||||
- 📟 系统托盘速度仪表显示实时速度 (Mac 专享)
|
||||
- 🌑 深色模式
|
||||
- 🗑 移除任务时可同时删除相关文件
|
||||
- 🌍 国际化,[查看已可选的语言](#-国际化)
|
||||
- 🎏 ...
|
||||
- 🛠 更多特性开发中
|
||||
|
||||
## 🖥 应用界面
|
||||
|
||||

|
||||

|
||||
|
||||
## ⌨️ 本地开发
|
||||
|
||||
@@ -89,12 +116,13 @@ git clone git@github.com:agalwood/Motrix.git
|
||||
|
||||
```bash
|
||||
cd Motrix
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
天朝大陆用户建议使用淘宝的 npm 源
|
||||
|
||||
```bash
|
||||
yarn config set registry 'https://registry.npm.taobao.org'
|
||||
npm config set registry 'https://registry.npm.taobao.org'
|
||||
export ELECTRON_MIRROR='https://npm.taobao.org/mirrors/electron/'
|
||||
export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
|
||||
@@ -104,18 +132,16 @@ export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
|
||||
|
||||
`Electron` 下载安装失败的问题,解决方式请参考 https://github.com/electron/electron/issues/8466#issuecomment-571425574
|
||||
|
||||
如果喜欢 [Yarn](https://yarnpkg.com/),也可以使用 `yarn` 安装依赖
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
### 编译打包
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
yarn run build
|
||||
```
|
||||
|
||||
完成之后可以在项目的 `release` 目录看到编译打包好的应用文件
|
||||
@@ -140,17 +166,27 @@ npm run build
|
||||
|
||||
| Key | Name | Status |
|
||||
|-------|:--------------------|:-------------|
|
||||
| ar | Arabic | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
|
||||
| bg | Българският език | ✔️ [@null-none](https://github.com/null-none) |
|
||||
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
|
||||
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
|
||||
| el | Ελληνικά | ✔️ [@Likecinema](https://github.com/Likecinema) |
|
||||
| en-US | English | ✔️ |
|
||||
| es | Español | ✔️ [@Chofito](https://github.com/Chofito)|
|
||||
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
|
||||
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
|
||||
| hu | Hungarian | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
|
||||
| id | Indonesia | ✔️ [@aarestu](https://github.com/aarestu) |
|
||||
| it | Italiano | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
|
||||
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
|
||||
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
|
||||
| pl | Polski | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
|
||||
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
|
||||
| ro | Română | ✔️ [@alyn3d](https://github.com/alyn3d) |
|
||||
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
|
||||
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
|
||||
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
|
||||
| vi | Tiếng Việt | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
|
||||
| zh-CN | 简体中文 | ✔️ |
|
||||
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Motrix
|
||||
|
||||
<a href="https://motrix.app">
|
||||
<img src="https://cdn.nlark.com/yuque/0/2018/png/129147/1543735425232-a5d2c99f-d788-43e4-9781-558ff6d21027.png" width="256" alt="App Icon" />
|
||||
<img src="./static/512x512.png" width="256" alt="App Icon" />
|
||||
</a>
|
||||
|
||||
## A full-featured download manager
|
||||
|
||||
[](https://github.com/agalwood/Motrix/releases)  [](https://travis-ci.com/agalwood/Motrix) [](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [](https://github.com/agalwood/Motrix/releases) 
|
||||
[](https://github.com/agalwood/Motrix/releases)   
|
||||
|
||||
English | [简体中文](./README-CN.md)
|
||||
|
||||
@@ -36,15 +36,42 @@ scoop install motrix
|
||||
The macOS users can install Motrix using `brew cask`, thanks to [PR](https://github.com/Homebrew/homebrew-cask/pull/59494) of [Mitscherlich](https://github.com/Mitscherlich).
|
||||
|
||||
```bash
|
||||
brew update && brew cask install motrix
|
||||
brew update && brew install --cask motrix
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
You can download the AppImage (for all Linux distributions) package or snap or just build from source code to install Motrix.
|
||||
You can download the `AppImage` (for all Linux distributions) or `snap` to install Motrix, see [GitHub/release](https://github.com/agalwood/Motrix/releases) for more Linux installation package formats.
|
||||
|
||||
Please read the **Build** section.
|
||||
If you want to build from source code, please read the **Build** section.
|
||||
|
||||
#### AppImage
|
||||
The latest version of Motrix AppImage requires you to manually perform desktop integration. Please check the documentation of [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) .
|
||||
|
||||
> Desktop Integration
|
||||
> Since electron-builder 21 desktop integration is not a part of produced AppImage file.
|
||||
> [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) is the recommended way to integrate AppImages.
|
||||
|
||||
Deepin 20 Beta users failed to install Motrix, please follow the steps below:
|
||||
|
||||
Open the `Terminal`, paste and run the following command to install Motrix again.
|
||||
|
||||
```bash
|
||||
sudo apt --fix-broken install
|
||||
```
|
||||
|
||||
#### Snap
|
||||
Motrix has been listed on [Snapcraft](https://snapcraft.io/motrix) , Ubuntu users recommend downloading from the Snap Store.
|
||||
|
||||
Tips for v1.5.10
|
||||
|
||||
The tray may not display the indicator normally, which makes it inconvenient to exit the application.
|
||||
|
||||
Please unchecked Preferences--Basic Settings--Hide App Menu (Windows & Linux Only), click Save & Apply. Then click "Exit" in the File menu to exit the application.
|
||||
|
||||
Please update to v1.5.12 and above, you can use the keyboard shortcut <kbd>Ctrl</kbd> + <kbd>q</kbd> to quickly exit the application.
|
||||
|
||||
#### AUR
|
||||
For Arch Linux users, Motrix is available in [aur](https://aur.archlinux.org/packages/motrix/), thanks to the maintainer [weearc](https://github.com/weearc).
|
||||
|
||||
Run the following command to install:
|
||||
@@ -60,7 +87,8 @@ Motrix may need to run with `sudo` for the first time in Linux because there is
|
||||
- 🕹 Simple and clear user interface
|
||||
- 🦄 Supports BitTorrent & Magnet
|
||||
- ☑️ BitTorrent selective download
|
||||
- 💾 Supports downloading BD Net Disk
|
||||
- 📡 Update tracker list every day automatically
|
||||
- 🔌 UPnP & NAT-PMP Port Mapping
|
||||
- 🎛 Up to 10 concurrent download tasks
|
||||
- 🚀 Supports 64 threads in a single task
|
||||
- 🚥 Supports speed limit
|
||||
@@ -68,14 +96,15 @@ Motrix may need to run with `sudo` for the first time in Linux because there is
|
||||
- 🔔 Download completed Notification
|
||||
- 💻 Ready for Touch Bar (Mac only)
|
||||
- 🤖 Resident system tray for quick operation
|
||||
- 📟 Tray speed meter displays real-time speed (Mac only)
|
||||
- 🌑 Dark mode
|
||||
- 🗑 Delete related files when removing tasks (optional)
|
||||
- 🌍 I18n, [View supported languages](#-internationalization).
|
||||
- 🎏 ...
|
||||
- 🛠 More features in development
|
||||
|
||||
## 🖥 User Interface
|
||||
|
||||

|
||||

|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
@@ -89,25 +118,23 @@ git clone git@github.com:agalwood/Motrix.git
|
||||
|
||||
```bash
|
||||
cd Motrix
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again
|
||||
|
||||
`Electron` failed to install correctly, please refer to https://github.com/electron/electron/issues/8466#issuecomment-571425574
|
||||
|
||||
If you like [Yarn](https://yarnpkg.com/), you can also use `yarn` to install dependencies.
|
||||
|
||||
### Dev Mode
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
### Build Release
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
yarn run build
|
||||
```
|
||||
|
||||
After building, the application will be found in the project's `release` directory.
|
||||
@@ -132,17 +159,27 @@ Translations into versions for other languages are welcome 🧐! Please read the
|
||||
|
||||
| Key | Name | Status |
|
||||
|-------|:--------------------|:-------------|
|
||||
| ar | Arabic | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
|
||||
| bg | Българският език | ✔️ [@null-none](https://github.com/null-none) |
|
||||
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
|
||||
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
|
||||
| el | Ελληνικά | ✔️ [@Likecinema](https://github.com/Likecinema) |
|
||||
| en-US | English | ✔️ |
|
||||
| es | Español | ✔️ [@Chofito](https://github.com/Chofito)|
|
||||
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
|
||||
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
|
||||
| hu | Hungarian | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
|
||||
| id | Indonesia | ✔️ [@aarestu](https://github.com/aarestu) |
|
||||
| it | Italiano | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
|
||||
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
|
||||
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
|
||||
| pl | Polski | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
|
||||
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
|
||||
| ro | Română | ✔️ [@alyn3d](https://github.com/alyn3d) |
|
||||
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
|
||||
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
|
||||
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
|
||||
| vi | Tiếng Việt | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
|
||||
| zh-CN | 简体中文 | ✔️ |
|
||||
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 62 KiB |
@@ -40,17 +40,21 @@ max-tries=5
|
||||
min-split-size=1M
|
||||
# Set user agent for HTTP(S) downloads.
|
||||
user-agent=Transmission/2.94
|
||||
# Send Accept: deflate, gzip request header
|
||||
http-accept-gzip=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
bt-force-encryption=true
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=255
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
|
||||
Binary file not shown.
@@ -40,17 +40,21 @@ max-tries=5
|
||||
min-split-size=1M
|
||||
# Set user agent for HTTP(S) downloads.
|
||||
user-agent=Transmission/2.94
|
||||
# Send Accept: deflate, gzip request header
|
||||
http-accept-gzip=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
bt-force-encryption=true
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=255
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
|
||||
@@ -40,17 +40,21 @@ max-tries=5
|
||||
min-split-size=1M
|
||||
# Set user agent for HTTP(S) downloads.
|
||||
user-agent=Transmission/2.94
|
||||
# Send Accept: deflate, gzip request header
|
||||
http-accept-gzip=true
|
||||
|
||||
|
||||
################ BT Task ################
|
||||
# Enable Local Peer Discovery.
|
||||
bt-enable-lpd=true
|
||||
# Requires BitTorrent message payload encryption with arc4.
|
||||
bt-force-encryption=true
|
||||
# bt-force-encryption=true
|
||||
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
|
||||
bt-hash-check-seed=true
|
||||
# Specify the maximum number of peers per torrent.
|
||||
bt-max-peers=255
|
||||
# Try to download first and last pieces of each file first. This is useful for previewing files.
|
||||
bt-prioritize-piece=head
|
||||
# Removes the unselected files when download is completed in BitTorrent.
|
||||
bt-remove-unselected-file=true
|
||||
# Seed previously downloaded files without verifying piece hashes.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/renderer/*"
|
||||
],
|
||||
"@shared/*": [
|
||||
"./src/shared/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
-15801
File diff suppressed because it is too large
Load Diff
+79
-68
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Motrix",
|
||||
"version": "1.5.7",
|
||||
"version": "1.6.9",
|
||||
"description": "A full-featured download manager",
|
||||
"homepage": "https://motrix.app",
|
||||
"author": {
|
||||
@@ -92,8 +92,20 @@
|
||||
},
|
||||
"mac": {
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "distribution",
|
||||
"darkModeSupport": true,
|
||||
@@ -181,84 +193,83 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@motrix/nat-api": "^0.3.1",
|
||||
"@panter/vue-i18next": "^0.15.2",
|
||||
"aria2": "^4.1.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.1",
|
||||
"bittorrent-peerid": "^1.3.3",
|
||||
"blob-util": "^2.0.2",
|
||||
"clipboard-polyfill": "^2.8.6",
|
||||
"electron-debug": "^3.0.1",
|
||||
"clipboard-polyfill": "^3.0.3",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-log": "^4.1.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.1",
|
||||
"element-ui": "^2.13.1",
|
||||
"forever-monitor": "1.7.2",
|
||||
"i18next": "^19.4.4",
|
||||
"lodash": "^4.17.15",
|
||||
"nat-api": "^0.1.3",
|
||||
"node-fetch": "^2.6.0",
|
||||
"electron-log": "^4.3.5",
|
||||
"electron-store": "^8.0.0",
|
||||
"electron-updater": "^4.3.8",
|
||||
"element-ui": "^2.15.1",
|
||||
"i18next": "^20.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.6.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"parse-torrent": "^7.1.2",
|
||||
"parse-torrent": "^9.1.3",
|
||||
"randomatic": "^3.1.1",
|
||||
"svg-innerhtml": "^1.1.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue": "^2.6.12",
|
||||
"vue-electron": "^1.0.6",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuex": "^3.3.0",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
"ws": "^7.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/register": "^7.9.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"ajv": "^6.12.2",
|
||||
"@babel/core": "^7.14.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.14.1",
|
||||
"@babel/register": "^7.13.16",
|
||||
"@electron/remote": "^1.1.0",
|
||||
"@motrix/multispinner": "^0.2.2",
|
||||
"@vue/eslint-config-standard": "^6.0.0",
|
||||
"ajv": "^8.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"cfonts": "^2.8.1",
|
||||
"chalk": "^4.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"del": "^5.1.0",
|
||||
"devtron": "^1.4.0",
|
||||
"electron": "^8.2.3",
|
||||
"electron-builder": "^22.6.0",
|
||||
"electron-builder-notarize": "^1.1.2",
|
||||
"electron-devtools-installer": "^3.0.0",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"electron-osx-sign": "^0.4.15",
|
||||
"eslint": "^6.8.0",
|
||||
"cfonts": "^2.9.1",
|
||||
"chalk": "^4.1.1",
|
||||
"copy-webpack-plugin": "^8.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.4",
|
||||
"css-minimizer-webpack-plugin": "^2.0.0",
|
||||
"del": "^6.0.0",
|
||||
"electron": "^11.4.5",
|
||||
"electron-builder": "22.10.5",
|
||||
"electron-builder-notarize": "^1.2.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-osx-sign": "^0.5.0",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^4.2.0",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"multispinner": "^0.2.1",
|
||||
"node-loader": "^0.6.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"style-loader": "^1.2.0",
|
||||
"terser-webpack-plugin": "^2.3.6",
|
||||
"url-loader": "^4.1.0",
|
||||
"vue-html-loader": "^1.2.4",
|
||||
"vue-loader": "^15.9.1",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"eslint-webpack-plugin": "^2.5.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "1.6.0",
|
||||
"node-loader": "^2.0.0",
|
||||
"sass": "^1.32.12",
|
||||
"sass-loader": "^11.0.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"terser-webpack-plugin": "^5.1.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^15.9.6",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^5.36.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
"webpack-merge": "^5.7.3",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
+2
-2
@@ -23,9 +23,9 @@
|
||||
</section>
|
||||
</div>
|
||||
<!-- Set `__static` path to static files in production -->
|
||||
<% if (!process.browser) { %>
|
||||
<% if (!htmlWebpackPlugin.options.isBrowser && !htmlWebpackPlugin.options.isDev) { %>
|
||||
<script>
|
||||
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
|
||||
+256
-77
@@ -1,13 +1,23 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { app, shell, dialog, ipcMain } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { readFile } from 'fs'
|
||||
import { readFile, unlink } from 'fs'
|
||||
import { extname, basename } from 'path'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import {
|
||||
APP_RUN_MODE,
|
||||
AUTO_SYNC_TRACKER_INTERVAL,
|
||||
AUTO_CHECK_UPDATE_INTERVAL
|
||||
} from '@shared/constants'
|
||||
import { checkIsNeedRun } from '@shared/utils'
|
||||
import {
|
||||
convertTrackerDataToComma,
|
||||
fetchBtTrackerFromSource
|
||||
} from '@shared/utils/tracker'
|
||||
import logger from './core/Logger'
|
||||
import ConfigManager from './core/ConfigManager'
|
||||
import { setupLocaleManager } from '@/ui/Locale'
|
||||
import { setupLocaleManager } from './ui/Locale'
|
||||
import Engine from './core/Engine'
|
||||
import EngineClient from './core/EngineClient'
|
||||
import UPnPManager from './core/UPnPManager'
|
||||
@@ -21,9 +31,7 @@ import TouchBarManager from './ui/TouchBarManager'
|
||||
import TrayManager from './ui/TrayManager'
|
||||
import DockManager from './ui/DockManager'
|
||||
import ThemeManager from './ui/ThemeManager'
|
||||
import { AUTO_SYNC_TRACKER_INTERVAL, AUTO_CHECK_UPDATE_INTERVAL } from '@shared/constants'
|
||||
import { checkIsNeedRun } from '@shared/utils'
|
||||
import { convertTrackerDataToComma, fetchBtTrackerFromSource } from '@shared/utils/tracker'
|
||||
import { getSessionPath } from './utils'
|
||||
|
||||
export default class Application extends EventEmitter {
|
||||
constructor () {
|
||||
@@ -33,45 +41,34 @@ export default class Application extends EventEmitter {
|
||||
}
|
||||
|
||||
init () {
|
||||
this.configManager = new ConfigManager()
|
||||
this.configManager = this.initConfigManager()
|
||||
|
||||
this.locale = this.configManager.getLocale()
|
||||
this.localeManager = setupLocaleManager(this.locale)
|
||||
this.i18n = this.localeManager.getI18n()
|
||||
|
||||
this.menuManager = new MenuManager()
|
||||
this.menuManager.setup(this.locale)
|
||||
|
||||
this.initTouchBarManager()
|
||||
this.setupApplicationMenu()
|
||||
|
||||
this.initWindowManager()
|
||||
|
||||
this.engine = new Engine({
|
||||
systemConfig: this.configManager.getSystemConfig(),
|
||||
userConfig: this.configManager.getUserConfig()
|
||||
})
|
||||
this.initUPnPManager()
|
||||
|
||||
this.startEngine()
|
||||
|
||||
this.initEngineClient()
|
||||
|
||||
this.initUPnPManager()
|
||||
this.initTouchBarManager()
|
||||
|
||||
this.autoSyncTracker()
|
||||
this.initThemeManager()
|
||||
|
||||
this.trayManager = new TrayManager({
|
||||
theme: this.configManager.getUserConfig('tray-theme')
|
||||
})
|
||||
this.initTrayManager()
|
||||
|
||||
this.dockManager = new DockManager({
|
||||
runMode: this.configManager.getUserConfig('run-mode')
|
||||
})
|
||||
this.initDockManager()
|
||||
|
||||
this.autoLaunchManager = new AutoLaunchManager()
|
||||
|
||||
this.energyManager = new EnergyManager()
|
||||
|
||||
this.initThemeManager()
|
||||
|
||||
this.initUpdaterManager()
|
||||
|
||||
this.initProtocolManager()
|
||||
@@ -81,12 +78,52 @@ export default class Application extends EventEmitter {
|
||||
this.handleEvents()
|
||||
|
||||
this.handleIpcMessages()
|
||||
|
||||
this.handleIpcInvokes()
|
||||
|
||||
this.emit('application:initialized')
|
||||
}
|
||||
|
||||
initConfigManager () {
|
||||
this.configListeners = {}
|
||||
return new ConfigManager()
|
||||
}
|
||||
|
||||
offConfigListeners () {
|
||||
try {
|
||||
Object.keys(this.configListeners).forEach((key) => {
|
||||
this.configListeners[key]()
|
||||
})
|
||||
} catch (e) {
|
||||
logger.warn('[Motrix] offConfigListeners===>', e)
|
||||
}
|
||||
this.configListeners = {}
|
||||
}
|
||||
|
||||
setupApplicationMenu () {
|
||||
this.menuManager = new MenuManager()
|
||||
this.menuManager.setup(this.locale)
|
||||
}
|
||||
|
||||
adjustMenu () {
|
||||
if (is.mas()) {
|
||||
const visibleStates = {
|
||||
'app.check-for-updates': false,
|
||||
'task.new-bt-task': false
|
||||
}
|
||||
this.menuManager.updateMenuStates(visibleStates, null, null)
|
||||
this.trayManager.updateMenuStates(visibleStates, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
startEngine () {
|
||||
const self = this
|
||||
|
||||
try {
|
||||
this.engine = new Engine({
|
||||
systemConfig: this.configManager.getSystemConfig(),
|
||||
userConfig: this.configManager.getUserConfig()
|
||||
})
|
||||
this.engine.start()
|
||||
} catch (err) {
|
||||
const { message } = err
|
||||
@@ -105,10 +142,13 @@ export default class Application extends EventEmitter {
|
||||
async stopEngine () {
|
||||
try {
|
||||
await this.engineClient.shutdown({ force: true })
|
||||
setImmediate(() => {
|
||||
this.engine.stop()
|
||||
})
|
||||
} catch (err) {
|
||||
logger.warn('[Motrix] shutdown engine fail: ', err.message)
|
||||
} finally {
|
||||
this.engine.stop()
|
||||
// no finally
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,15 +161,56 @@ export default class Application extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
initTrayManager () {
|
||||
this.trayManager = new TrayManager({
|
||||
theme: this.configManager.getUserConfig('tray-theme'),
|
||||
systemTheme: this.themeManager.getSystemTheme(),
|
||||
speedometer: this.configManager.getUserConfig('tray-speedometer')
|
||||
})
|
||||
|
||||
this.watchTraySpeedometerEnabledChange()
|
||||
|
||||
this.trayManager.on('mouse-down', ({ focused }) => {
|
||||
this.sendCommandToAll('application:update-tray-focused', { focused })
|
||||
})
|
||||
|
||||
this.trayManager.on('mouse-up', ({ focused }) => {
|
||||
this.sendCommandToAll('application:update-tray-focused', { focused })
|
||||
})
|
||||
|
||||
this.trayManager.on('drop-files', (files = []) => {
|
||||
this.handleFile(files[0])
|
||||
})
|
||||
|
||||
this.trayManager.on('drop-text', (text) => {
|
||||
this.handleProtocol(text)
|
||||
})
|
||||
}
|
||||
|
||||
watchTraySpeedometerEnabledChange () {
|
||||
const { userConfig } = this.configManager
|
||||
const key = 'tray-speedometer'
|
||||
this.configListeners[key] = userConfig.onDidChange('tray-speedometer', async (newValue, oldValue) => {
|
||||
logger.info('[Motrix] detected tray speedometer value change event:', newValue, oldValue)
|
||||
this.trayManager.handleSpeedometerEnableChange(newValue)
|
||||
})
|
||||
}
|
||||
|
||||
initDockManager () {
|
||||
this.dockManager = new DockManager({
|
||||
runMode: this.configManager.getUserConfig('run-mode')
|
||||
})
|
||||
}
|
||||
|
||||
initUPnPManager () {
|
||||
this.upnp = new UPnPManager()
|
||||
|
||||
this.watchEnableUPnPChange()
|
||||
this.watchUPnPEnabledChange()
|
||||
|
||||
this.watchPortsChange()
|
||||
this.watchUPnPPortsChange()
|
||||
|
||||
const enable = this.configManager.getUserConfig('enable-upnp')
|
||||
if (!enable) {
|
||||
const enabled = this.configManager.getUserConfig('enable-upnp')
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -145,7 +226,7 @@ export default class Application extends EventEmitter {
|
||||
this.upnp.map(dhtPort)
|
||||
]
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
await Promise.allSettled(promises)
|
||||
} catch (e) {
|
||||
logger.warn('[Motrix] start UPnP mapping fail', e)
|
||||
}
|
||||
@@ -160,17 +241,18 @@ export default class Application extends EventEmitter {
|
||||
this.upnp.unmap(dhtPort)
|
||||
]
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
await Promise.allSettled(promises)
|
||||
} catch (e) {
|
||||
logger.warn('[Motrix] stop UPnP mapping fail', e)
|
||||
}
|
||||
}
|
||||
|
||||
watchPortsChange () {
|
||||
watchUPnPPortsChange () {
|
||||
const { systemConfig } = this.configManager
|
||||
const watchKeys = ['listen-port', 'dht-listen-port']
|
||||
|
||||
watchKeys.map((key) => {
|
||||
this.configManager.systemConfig.onDidChange(key, async (newValue, oldValue) => {
|
||||
watchKeys.forEach((key) => {
|
||||
this.configListeners[key] = systemConfig.onDidChange(key, async (newValue, oldValue) => {
|
||||
logger.info('[Motrix] detected port change event:', key, newValue, oldValue)
|
||||
const enable = this.configManager.getUserConfig('enable-upnp')
|
||||
if (!enable) {
|
||||
@@ -182,7 +264,7 @@ export default class Application extends EventEmitter {
|
||||
this.upnp.map(newValue)
|
||||
]
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
await Promise.allSettled(promises)
|
||||
} catch (e) {
|
||||
logger.info('[Motrix] change UPnP port mapping failed:', e)
|
||||
}
|
||||
@@ -190,13 +272,16 @@ export default class Application extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
watchEnableUPnPChange () {
|
||||
this.configManager.userConfig.onDidChange('enable-upnp', async (newValue, oldValue) => {
|
||||
watchUPnPEnabledChange () {
|
||||
const { userConfig } = this.configManager
|
||||
const key = 'enable-upnp'
|
||||
this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
|
||||
logger.info('[Motrix] detected enable-upnp value change event:', newValue, oldValue)
|
||||
if (newValue) {
|
||||
this.startUPnPMapping()
|
||||
} else {
|
||||
this.stopUPnPMapping()
|
||||
await this.stopUPnPMapping()
|
||||
this.upnp.closeClient()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -207,20 +292,30 @@ export default class Application extends EventEmitter {
|
||||
await this.stopUPnPMapping()
|
||||
}
|
||||
|
||||
this.upnp.destroy()
|
||||
this.upnp.closeClient()
|
||||
}
|
||||
|
||||
autoSyncTracker () {
|
||||
const enable = this.configManager.getUserConfig('auto-sync-tracker')
|
||||
const lastTime = this.configManager.getUserConfig('last-sync-tracker-time')
|
||||
const result = checkIsNeedRun(enable, lastTime, AUTO_SYNC_TRACKER_INTERVAL)
|
||||
logger.info('[Motrix] auto sync tracker checkIsNeedRun:', result)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
const source = this.configManager.getUserConfig('tracker-source')
|
||||
if (isEmpty(source)) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const source = this.configManager.getUserConfig('tracker-source')
|
||||
fetchBtTrackerFromSource(source).then((data) => {
|
||||
logger.warn('[Motrix] auto sync tracker data:', data)
|
||||
if (!data || data.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const tracker = convertTrackerDataToComma(data)
|
||||
this.savePreference({
|
||||
system: {
|
||||
@@ -230,8 +325,19 @@ export default class Application extends EventEmitter {
|
||||
'last-sync-tracker-time': Date.now()
|
||||
}
|
||||
})
|
||||
}).catch((err) => {
|
||||
logger.warn('[Motrix] auto sync tracker failed:', err.message)
|
||||
})
|
||||
}, 3000)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
autoResumeTask () {
|
||||
const enabled = this.configManager.getUserConfig('resume-all-when-app-launched')
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.engineClient.call('unpauseAll')
|
||||
}
|
||||
|
||||
initWindowManager () {
|
||||
@@ -242,12 +348,25 @@ export default class Application extends EventEmitter {
|
||||
this.windowManager.on('window-resized', (data) => {
|
||||
this.storeWindowState(data)
|
||||
})
|
||||
|
||||
this.windowManager.on('window-moved', (data) => {
|
||||
this.storeWindowState(data)
|
||||
})
|
||||
|
||||
this.windowManager.on('window-closed', (data) => {
|
||||
this.storeWindowState(data)
|
||||
})
|
||||
|
||||
this.windowManager.on('enter-full-screen', (window) => {
|
||||
this.dockManager.show()
|
||||
})
|
||||
|
||||
this.windowManager.on('leave-full-screen', (window) => {
|
||||
const mode = this.configManager.getUserConfig('run-mode')
|
||||
if (mode !== APP_RUN_MODE.STANDARD) {
|
||||
this.dockManager.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
storeWindowState (data = {}) {
|
||||
@@ -291,7 +410,7 @@ export default class Application extends EventEmitter {
|
||||
|
||||
hide (page) {
|
||||
if (page) {
|
||||
this.windowManager.autoHideWindow(page)
|
||||
this.windowManager.hideWindow(page)
|
||||
} else {
|
||||
this.windowManager.hideAllWindow()
|
||||
}
|
||||
@@ -311,9 +430,9 @@ export default class Application extends EventEmitter {
|
||||
|
||||
this.energyManager.stopPowerSaveBlocker()
|
||||
|
||||
this.trayManager.destroy()
|
||||
|
||||
await this.stopEngine()
|
||||
|
||||
this.trayManager.destroy()
|
||||
} catch (err) {
|
||||
logger.warn('[Motrix] stop error: ', err.message)
|
||||
}
|
||||
@@ -349,9 +468,9 @@ export default class Application extends EventEmitter {
|
||||
|
||||
initThemeManager () {
|
||||
this.themeManager = new ThemeManager()
|
||||
this.themeManager.on('system-theme-changed', (theme) => {
|
||||
this.trayManager.changeIconTheme(theme)
|
||||
this.sendCommandToAll('application:system-theme', theme)
|
||||
this.themeManager.on('system-theme-change', (theme) => {
|
||||
this.trayManager.handleSystemThemeChange(theme)
|
||||
this.sendCommandToAll('application:update-system-theme', { theme })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,13 +478,11 @@ export default class Application extends EventEmitter {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.touchBarManager = new TouchBarManager()
|
||||
}
|
||||
|
||||
initProtocolManager () {
|
||||
if (is.dev() || is.mas()) {
|
||||
return
|
||||
}
|
||||
const protocols = this.configManager.getUserConfig('protocols', {})
|
||||
this.protocolManager = new ProtocolManager({
|
||||
protocols
|
||||
@@ -373,10 +490,6 @@ export default class Application extends EventEmitter {
|
||||
}
|
||||
|
||||
handleProtocol (url) {
|
||||
if (is.dev() || is.mas()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.show()
|
||||
|
||||
this.protocolManager.handle(url)
|
||||
@@ -393,15 +506,17 @@ export default class Application extends EventEmitter {
|
||||
|
||||
this.show()
|
||||
|
||||
const fileName = basename(filePath)
|
||||
const name = basename(filePath)
|
||||
readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] read file error: ${filePath}`, err.message)
|
||||
return
|
||||
}
|
||||
const file = Buffer.from(data).toString('base64')
|
||||
const args = [fileName, file]
|
||||
this.sendCommandToAll('application:new-bt-task-with-file', ...args)
|
||||
const dataURL = Buffer.from(data).toString('base64')
|
||||
this.sendCommandToAll('application:new-bt-task-with-file', {
|
||||
name,
|
||||
dataURL
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -410,10 +525,10 @@ export default class Application extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const enable = this.configManager.getUserConfig('auto-check-update')
|
||||
const enabled = this.configManager.getUserConfig('auto-check-update')
|
||||
const lastTime = this.configManager.getUserConfig('last-check-update-time')
|
||||
this.updateManager = new UpdateManager({
|
||||
autoCheck: checkIsNeedRun(enable, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
|
||||
autoCheck: checkIsNeedRun(enabled, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
|
||||
})
|
||||
this.handleUpdaterEvents()
|
||||
}
|
||||
@@ -452,12 +567,27 @@ export default class Application extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
relaunch () {
|
||||
this.stop()
|
||||
async relaunch () {
|
||||
await this.stop()
|
||||
app.relaunch()
|
||||
app.exit()
|
||||
}
|
||||
|
||||
async resetSession () {
|
||||
await this.stopEngine()
|
||||
|
||||
app.clearRecentDocuments()
|
||||
|
||||
const sessionPath = this.configManager.getUserConfig('session-path') || getSessionPath()
|
||||
setTimeout(() => {
|
||||
unlink(sessionPath, function (err) {
|
||||
logger.info('[Motrix] Removed the download seesion file:', err)
|
||||
})
|
||||
|
||||
this.engine.start()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
savePreference (config = {}) {
|
||||
logger.info('[Motrix] save preference:', config)
|
||||
const { system, user } = config
|
||||
@@ -476,6 +606,10 @@ export default class Application extends EventEmitter {
|
||||
handleCommands () {
|
||||
this.on('application:save-preference', this.savePreference)
|
||||
|
||||
this.on('application:update-tray', (tray) => {
|
||||
this.trayManager.updateTrayByImage(tray)
|
||||
})
|
||||
|
||||
this.on('application:relaunch', () => {
|
||||
this.relaunch()
|
||||
})
|
||||
@@ -496,15 +630,18 @@ export default class Application extends EventEmitter {
|
||||
}
|
||||
})
|
||||
|
||||
this.on('application:show', (page) => {
|
||||
this.on('application:show', ({ page }) => {
|
||||
this.show(page)
|
||||
})
|
||||
|
||||
this.on('application:hide', (page) => {
|
||||
this.on('application:hide', ({ page }) => {
|
||||
this.hide(page)
|
||||
})
|
||||
|
||||
this.on('application:reset-session', () => this.resetSession())
|
||||
|
||||
this.on('application:reset', () => {
|
||||
this.offConfigListeners()
|
||||
this.configManager.reset()
|
||||
this.relaunch()
|
||||
})
|
||||
@@ -515,14 +652,14 @@ export default class Application extends EventEmitter {
|
||||
|
||||
this.on('application:change-theme', (theme) => {
|
||||
this.themeManager.updateAppAppearance(theme)
|
||||
this.sendCommandToAll('application:theme', theme)
|
||||
this.sendCommandToAll('application:update-theme', { theme })
|
||||
})
|
||||
|
||||
this.on('application:change-locale', (locale) => {
|
||||
this.localeManager.changeLanguageByLocale(locale)
|
||||
.then(() => {
|
||||
this.menuManager.setup(locale)
|
||||
this.trayManager.setup(locale)
|
||||
this.menuManager.handleLocaleChange(locale)
|
||||
this.trayManager.handleLocaleChange(locale)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -580,34 +717,57 @@ export default class Application extends EventEmitter {
|
||||
this.protocolManager.setup(protocols)
|
||||
})
|
||||
|
||||
this.on('application:open-external', (url) => {
|
||||
this.openExternal(url)
|
||||
})
|
||||
|
||||
this.on('help:official-website', () => {
|
||||
const url = 'https://motrix.app/'
|
||||
shell.openExternal(url)
|
||||
this.openExternal(url)
|
||||
})
|
||||
|
||||
this.on('help:manual', () => {
|
||||
const url = 'https://motrix.app/manual'
|
||||
shell.openExternal(url)
|
||||
this.openExternal(url)
|
||||
})
|
||||
|
||||
this.on('help:release-notes', () => {
|
||||
const url = 'https://motrix.app/release'
|
||||
shell.openExternal(url)
|
||||
this.openExternal(url)
|
||||
})
|
||||
|
||||
this.on('help:report-problem', () => {
|
||||
const url = 'https://motrix.app/report'
|
||||
shell.openExternal(url)
|
||||
this.openExternal(url)
|
||||
})
|
||||
}
|
||||
|
||||
openExternal (url) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
shell.openExternal(url)
|
||||
}
|
||||
|
||||
handleConfigChange (configName) {
|
||||
this.sendCommandToAll('application:update-preference-config', { configName })
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
// this.configManager.systemConfig.onDidAnyChange(() => {
|
||||
// this.engineClient.changeGlobalOption(this.configManager.getSystemConfig())
|
||||
// })
|
||||
this.once('application:initialized', () => {
|
||||
this.autoSyncTracker()
|
||||
|
||||
this.autoResumeTask()
|
||||
|
||||
this.adjustMenu()
|
||||
})
|
||||
|
||||
this.configManager.userConfig.onDidAnyChange(() => this.handleConfigChange('user'))
|
||||
this.configManager.systemConfig.onDidAnyChange(() => this.handleConfigChange('system'))
|
||||
|
||||
this.on('download-status-change', (downloading) => {
|
||||
this.trayManager.updateTrayByStatus(downloading)
|
||||
this.trayManager.handleDownloadStatusChange(downloading)
|
||||
if (downloading) {
|
||||
this.energyManager.startPowerSaveBlocker()
|
||||
} else {
|
||||
@@ -615,12 +775,18 @@ export default class Application extends EventEmitter {
|
||||
}
|
||||
})
|
||||
|
||||
this.on('download-speed-change', (speed) => {
|
||||
this.dockManager.setBadge(speed)
|
||||
this.on('speed-change', (speed) => {
|
||||
this.dockManager.handleSpeedChange(speed)
|
||||
this.trayManager.handleSpeedChange(speed)
|
||||
})
|
||||
|
||||
this.on('task-download-complete', (task, path) => {
|
||||
this.dockManager.openDock(path)
|
||||
|
||||
if (is.linux()) {
|
||||
return
|
||||
}
|
||||
app.addRecentDocument(path)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -635,4 +801,17 @@ export default class Application extends EventEmitter {
|
||||
this.emit(eventName, ...args)
|
||||
})
|
||||
}
|
||||
|
||||
handleIpcInvokes () {
|
||||
ipcMain.handle('get-app-config', async () => {
|
||||
const systemConfig = this.configManager.getSystemConfig()
|
||||
const userConfig = this.configManager.getUserConfig()
|
||||
|
||||
const result = {
|
||||
...systemConfig,
|
||||
...userConfig
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export default {
|
||||
transparent: !is.windows()
|
||||
},
|
||||
bindCloseToHide: true,
|
||||
url: is.dev() ? 'http://localhost:9080' : `file://${__dirname}/index.html`
|
||||
url: is.dev() ? 'http://localhost:9080' : require('path').join('file://', __dirname, '/index.html')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
import { LOGIN_SETTING_OPTIONS } from '@shared/constants'
|
||||
|
||||
export default class AutoLaunchManager {
|
||||
enable () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const enabled = app.getLoginItemSettings().openAtLogin
|
||||
const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
|
||||
if (enabled) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
// For Windows
|
||||
args: [
|
||||
'--opened-at-login=1'
|
||||
]
|
||||
...LOGIN_SETTING_OPTIONS,
|
||||
openAtLogin: true
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
@@ -28,7 +27,7 @@ export default class AutoLaunchManager {
|
||||
|
||||
isEnabled () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const enabled = app.getLoginItemSettings().openAtLogin
|
||||
const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
|
||||
resolve(enabled)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
APP_RUN_MODE,
|
||||
APP_THEME,
|
||||
EMPTY_STRING,
|
||||
NGOSANG_TRACKERS_BEST_IP_URL,
|
||||
NGOSANG_TRACKERS_BEST_URL,
|
||||
IP_VERSION
|
||||
IP_VERSION,
|
||||
LOGIN_SETTING_OPTIONS,
|
||||
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
|
||||
NGOSANG_TRACKERS_BEST_URL_CDN
|
||||
} from '@shared/constants'
|
||||
import { separateConfig } from '@shared/utils'
|
||||
|
||||
@@ -49,6 +50,9 @@ export default class ConfigManager {
|
||||
'all-proxy': EMPTY_STRING,
|
||||
'allow-overwrite': false,
|
||||
'auto-file-renaming': true,
|
||||
'bt-exclude-tracker': EMPTY_STRING,
|
||||
'bt-load-saved-metadata': true,
|
||||
'bt-save-metadata': true,
|
||||
'bt-tracker': EMPTY_STRING,
|
||||
'continue': true,
|
||||
'dht-file-path': getDhtPath(IP_VERSION.V4),
|
||||
@@ -68,7 +72,7 @@ export default class ConfigManager {
|
||||
'rpc-secret': EMPTY_STRING,
|
||||
'seed-ratio': 1,
|
||||
'seed-time': 60,
|
||||
'split': 128,
|
||||
'split': getMaxConnectionPerServer(),
|
||||
'user-agent': 'Transmission/2.94'
|
||||
}
|
||||
/* eslint-enable quote-props */
|
||||
@@ -96,12 +100,14 @@ export default class ConfigManager {
|
||||
'enable-upnp': true,
|
||||
'engine-max-connection-per-server': getMaxConnectionPerServer(),
|
||||
'hide-app-menu': is.windows() || is.linux(),
|
||||
'keep-seeding': false,
|
||||
'keep-window-state': false,
|
||||
'last-check-update-time': 0,
|
||||
'last-sync-tracker-time': 0,
|
||||
'locale': app.getLocale(),
|
||||
'log-path': getLogPath(),
|
||||
'new-task-show-downloading': true,
|
||||
'no-confirm-before-delete-task': false,
|
||||
'open-at-login': false,
|
||||
'protocols': { 'magnet': true, 'thunder': false },
|
||||
'resume-all-when-app-launched': false,
|
||||
@@ -110,10 +116,11 @@ export default class ConfigManager {
|
||||
'task-notification': true,
|
||||
'theme': APP_THEME.AUTO,
|
||||
'tracker-source': [
|
||||
NGOSANG_TRACKERS_BEST_IP_URL,
|
||||
NGOSANG_TRACKERS_BEST_URL
|
||||
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
|
||||
NGOSANG_TRACKERS_BEST_URL_CDN
|
||||
],
|
||||
'tray-theme': APP_THEME.AUTO,
|
||||
'tray-speedometer': is.macOS(),
|
||||
'update-channel': 'latest',
|
||||
'use-proxy': false,
|
||||
'window-state': {}
|
||||
@@ -138,15 +145,15 @@ export default class ConfigManager {
|
||||
fixUserConfig () {
|
||||
// Fix the value of open-at-login when the user delete
|
||||
// the Motrix self-starting item through startup management.
|
||||
const openAtLogin = app.getLoginItemSettings().openAtLogin
|
||||
const openAtLogin = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
|
||||
if (this.getUserConfig('open-at-login') !== openAtLogin) {
|
||||
this.setUserConfig('open-at-login', openAtLogin)
|
||||
}
|
||||
|
||||
if (this.getUserConfig('tracker-source').length === 0) {
|
||||
this.setUserConfig('tracker-source', [
|
||||
NGOSANG_TRACKERS_BEST_IP_URL,
|
||||
NGOSANG_TRACKERS_BEST_URL
|
||||
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
|
||||
NGOSANG_TRACKERS_BEST_URL_CDN
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
+93
-79
@@ -1,18 +1,22 @@
|
||||
import { app } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { existsSync } from 'fs'
|
||||
import { existsSync, writeFile, unlink } from 'fs'
|
||||
import { resolve, join } from 'path'
|
||||
import forever from 'forever-monitor'
|
||||
import { spawn } from 'child_process'
|
||||
|
||||
import logger from './Logger'
|
||||
import { getI18n } from '@/ui/Locale'
|
||||
import { getI18n } from '../ui/Locale'
|
||||
import {
|
||||
getEngineBin,
|
||||
getEnginePidPath,
|
||||
getSessionPath,
|
||||
transformConfig
|
||||
} from '../utils/index'
|
||||
|
||||
const { platform } = process
|
||||
|
||||
export default class Engine {
|
||||
// ChildProcess | null
|
||||
static instance = null
|
||||
|
||||
constructor (options = {}) {
|
||||
@@ -21,84 +25,116 @@ export default class Engine {
|
||||
this.i18n = getI18n()
|
||||
this.systemConfig = options.systemConfig
|
||||
this.userConfig = options.userConfig
|
||||
this.basePath = this.getBasePath()
|
||||
}
|
||||
|
||||
getStartSh () {
|
||||
const { platform } = process
|
||||
let basePath = resolve(app.getAppPath(), '..')
|
||||
start () {
|
||||
const pidPath = getEnginePidPath()
|
||||
logger.info('[Motrix] Engie pid path:', pidPath)
|
||||
|
||||
if (is.dev()) {
|
||||
basePath = resolve(__dirname, `../../../extra/${platform}`)
|
||||
if (this.instance) {
|
||||
return
|
||||
}
|
||||
|
||||
const binPath = this.getBinPath()
|
||||
const args = this.getStartArgs()
|
||||
this.instance = spawn(binPath, args, {
|
||||
windowsHide: false,
|
||||
stdio: is.dev() ? 'pipe' : 'ignore'
|
||||
})
|
||||
const pid = this.instance.pid.toString()
|
||||
this.writePidFile(pidPath, pid)
|
||||
|
||||
this.instance.once('close', function () {
|
||||
try {
|
||||
unlink(pidPath, function (err) {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (is.dev()) {
|
||||
this.instance.stdout.on('data', function (data) {
|
||||
logger.log('[Motrix] engine stdout===>', data.toString())
|
||||
})
|
||||
|
||||
this.instance.stderr.on('data', function (data) {
|
||||
logger.log('[Motrix] engine stderr===>', data.toString())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this.instance) {
|
||||
this.instance.kill()
|
||||
this.instance = null
|
||||
}
|
||||
}
|
||||
|
||||
writePidFile (pidPath, pid) {
|
||||
writeFile(pidPath, pid, (err) => {
|
||||
if (err) {
|
||||
logger.error(`[Motrix] Write engine process pid failed: ${err}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getBinPath () {
|
||||
const binName = getEngineBin(platform)
|
||||
if (!binName) {
|
||||
throw new Error(this.i18n.t('app.engine-damaged-message'))
|
||||
}
|
||||
|
||||
const binPath = join(basePath, `/engine/${binName}`)
|
||||
const binIsExist = existsSync(binPath)
|
||||
const result = join(this.basePath, `/engine/${binName}`)
|
||||
const binIsExist = existsSync(result)
|
||||
if (!binIsExist) {
|
||||
logger.error('[Motrix] engine bin is not exist:', binPath)
|
||||
logger.error('[Motrix] engine bin is not exist:', result)
|
||||
throw new Error(this.i18n.t('app.engine-missing-message'))
|
||||
}
|
||||
|
||||
const confPath = join(basePath, '/engine/aria2.conf')
|
||||
|
||||
const sessionPath = this.userConfig['session-path'] || getSessionPath()
|
||||
const sessionIsExist = existsSync(sessionPath)
|
||||
|
||||
let result = [`${binPath}`, `--conf-path=${confPath}`, `--save-session=${sessionPath}`]
|
||||
if (sessionIsExist) {
|
||||
result = [...result, `--input-file=${sessionPath}`]
|
||||
}
|
||||
|
||||
const extraConfig = transformConfig(this.systemConfig)
|
||||
result = [...result, ...extraConfig]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
start () {
|
||||
const sh = this.getStartSh()
|
||||
logger.info('[Motrix] Engine start sh:', sh)
|
||||
this.instance = forever.start(sh, {
|
||||
max: is.dev() ? 1 : 100,
|
||||
parser: function (command, args) {
|
||||
return {
|
||||
command: command,
|
||||
args: args
|
||||
}
|
||||
},
|
||||
silent: !is.dev()
|
||||
})
|
||||
getBasePath () {
|
||||
let result = resolve(app.getAppPath(), '..')
|
||||
|
||||
const { child } = this.instance
|
||||
logger.info('[Motrix] Engine pid:', child.pid)
|
||||
if (is.dev()) {
|
||||
result = resolve(__dirname, `../../../extra/${platform}`)
|
||||
}
|
||||
|
||||
this.instance.on('error', (err) => {
|
||||
logger.info(`[Motrix] Engine error: ${err}`)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
this.instance.on('start', function (process, data) {
|
||||
logger.info('[Motrix] Engine started')
|
||||
})
|
||||
getStartArgs () {
|
||||
const confPath = join(this.basePath, '/engine/aria2.conf')
|
||||
|
||||
this.instance.on('stop', function (process) {
|
||||
logger.info('[Motrix] Engine stopped')
|
||||
})
|
||||
const sessionPath = this.userConfig['session-path'] || getSessionPath()
|
||||
const sessionIsExist = existsSync(sessionPath)
|
||||
|
||||
// this.instance.on('restart', function (forever) {
|
||||
// logger.info(`[Motrix] Engine exit:`)
|
||||
// })
|
||||
let result = [`--conf-path=${confPath}`, `--save-session=${sessionPath}`]
|
||||
if (sessionIsExist) {
|
||||
result = [...result, `--input-file=${sessionPath}`]
|
||||
}
|
||||
|
||||
// this.instance.on('exit:code', function (code) {
|
||||
// logger.info(`[Motrix] Engine exit: ${code}`)
|
||||
// })
|
||||
const extraConfig = {
|
||||
...this.systemConfig
|
||||
}
|
||||
const keepSeeding = this.userConfig['keep-seeding']
|
||||
const seedRatio = this.systemConfig['seed-ratio']
|
||||
if (keepSeeding || seedRatio === 0) {
|
||||
extraConfig['seed-ratio'] = 0
|
||||
delete extraConfig['seed-time']
|
||||
}
|
||||
console.log('extraConfig===>', extraConfig)
|
||||
|
||||
// this.instance.on('stderr', (data) => {
|
||||
// logger.info(`[Motrix] Engine stderr: ${data}`)
|
||||
// })
|
||||
const extra = transformConfig(extraConfig)
|
||||
result = [...result, ...extra]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
isRunning (pid) {
|
||||
@@ -109,28 +145,6 @@ export default class Engine {
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
const { pid } = this.instance.child
|
||||
try {
|
||||
logger.info('[Motrix] Engine stopping')
|
||||
this.instance.stop()
|
||||
} catch (err) {
|
||||
logger.error('[Motrix] Engine stop fail:', err.message)
|
||||
this.forceStop(pid)
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
forceStop (pid) {
|
||||
try {
|
||||
if (pid && this.isRunning(pid)) {
|
||||
process.kill(pid)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('[Motrix] Engine force stop fail:', err)
|
||||
}
|
||||
}
|
||||
|
||||
restart () {
|
||||
this.stop()
|
||||
this.start()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
import Aria2 from 'aria2'
|
||||
import { Aria2 } from '@shared/aria2'
|
||||
|
||||
import logger from './Logger'
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import { app } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { parse } from 'querystring'
|
||||
|
||||
import logger from './Logger'
|
||||
import protocolMap from '../configs/protocol'
|
||||
@@ -28,6 +29,10 @@ export default class ProtocolManager extends EventEmitter {
|
||||
}
|
||||
|
||||
setup (protocols) {
|
||||
if (is.dev() || is.mas()) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.keys(protocols).forEach((protocol) => {
|
||||
const enabled = protocols[protocol]
|
||||
if (enabled) {
|
||||
@@ -44,10 +49,13 @@ export default class ProtocolManager extends EventEmitter {
|
||||
logger.info(`[Motrix] protocol url: ${url}`)
|
||||
|
||||
if (
|
||||
url.toLowerCase().startsWith('ftp:') ||
|
||||
url.toLowerCase().startsWith('http:') ||
|
||||
url.toLowerCase().startsWith('https:') ||
|
||||
url.toLowerCase().startsWith('magnet:') ||
|
||||
url.toLowerCase().startsWith('thunder:')
|
||||
) {
|
||||
return this.handleMagnetAndThunderProtocol(url)
|
||||
return this.handleResourceProtocol(url)
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -58,17 +66,20 @@ export default class ProtocolManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleMagnetAndThunderProtocol (url) {
|
||||
handleResourceProtocol (url) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
global.application.sendCommandToAll('application:new-task', ADD_TASK_TYPE.URI, url)
|
||||
global.application.sendCommandToAll('application:new-task', {
|
||||
type: ADD_TASK_TYPE.URI,
|
||||
uri: url
|
||||
})
|
||||
}
|
||||
|
||||
handleMoProtocol (url) {
|
||||
const parsed = new URL(url)
|
||||
const { host } = parsed
|
||||
const { host, search } = parsed
|
||||
logger.info('[Motrix] protocol parsed:', parsed, host)
|
||||
|
||||
const command = protocolMap[host]
|
||||
@@ -76,10 +87,8 @@ export default class ProtocolManager extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO 没想明白怎么传参数好
|
||||
// 如果按顺序传递,那 url 的 query string 就要求有序的了
|
||||
// const query = queryString.parse(parsed.query)
|
||||
const args = []
|
||||
global.application.sendCommandToAll(command, ...args)
|
||||
const query = search.startsWith('?') ? search.replace('?', '') : search
|
||||
const args = parse(query)
|
||||
global.application.sendCommandToAll(command, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import NatAPI from 'nat-api'
|
||||
import NatAPI from '@motrix/nat-api'
|
||||
|
||||
import logger from './Logger'
|
||||
|
||||
@@ -17,7 +17,9 @@ export default class UPnPManager {
|
||||
return
|
||||
}
|
||||
|
||||
client = new NatAPI()
|
||||
client = new NatAPI({
|
||||
autoUpdate: true
|
||||
})
|
||||
}
|
||||
|
||||
map (port) {
|
||||
@@ -30,17 +32,21 @@ export default class UPnPManager {
|
||||
return
|
||||
}
|
||||
|
||||
client.map(port, (err) => {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err)
|
||||
reject(err.message)
|
||||
return
|
||||
}
|
||||
try {
|
||||
client.map(port, (err) => {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err)
|
||||
reject(err.message)
|
||||
return
|
||||
}
|
||||
|
||||
mappingStatus[port] = true
|
||||
logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
|
||||
resolve()
|
||||
})
|
||||
mappingStatus[port] = true
|
||||
logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
|
||||
resolve()
|
||||
})
|
||||
} catch (err) {
|
||||
reject(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,26 +65,35 @@ export default class UPnPManager {
|
||||
return
|
||||
}
|
||||
|
||||
client.unmap(port, (err) => {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
|
||||
reject(err.message)
|
||||
return
|
||||
}
|
||||
try {
|
||||
client.unmap(port, (err) => {
|
||||
if (err) {
|
||||
logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
|
||||
reject(err.message)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
|
||||
mappingStatus[port] = false
|
||||
resolve()
|
||||
})
|
||||
logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
|
||||
mappingStatus[port] = false
|
||||
resolve()
|
||||
})
|
||||
} catch (err) {
|
||||
reject(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
closeClient () {
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
client.destroy()
|
||||
client = null
|
||||
try {
|
||||
client.destroy(() => {
|
||||
client = null
|
||||
})
|
||||
} catch (err) {
|
||||
logger.warn('[Motrix] close UPnP client fail', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import is from 'electron-is'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
|
||||
import logger from './Logger'
|
||||
import { getI18n } from '@/ui/Locale'
|
||||
import { getI18n } from '../ui/Locale'
|
||||
|
||||
if (is.dev()) {
|
||||
autoUpdater.updateConfigPath = resolve(__dirname, '../../../app-update.yml')
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { app } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { initialize } from '@electron/remote/main'
|
||||
|
||||
import Launcher from './Launcher'
|
||||
|
||||
/**
|
||||
* initialize the main-process side of the remote module
|
||||
*/
|
||||
initialize()
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"id": "menu.app",
|
||||
"submenu": [
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.preferences", "command": "application:preferences" },
|
||||
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
|
||||
@@ -11,15 +11,15 @@
|
||||
{ "id": "app.hide-others", "role": "hideothers" },
|
||||
{ "id": "app.unhide", "role": "unhide" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.quit", "command": "application:quit" }
|
||||
{ "id": "app.quit", "role": "quit" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "menu.task",
|
||||
"submenu": [
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "task.pause-task", "command": "application:pause-task" },
|
||||
{ "id": "task.resume-task", "command": "application:resume-task" },
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
{
|
||||
"id": "menu.file",
|
||||
"submenu": [
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.preferences", "command": "application:preferences" },
|
||||
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.quit", "command": "application:quit" }
|
||||
{ "id": "app.quit", "role": "quit" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "menu.task",
|
||||
"submenu": [
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "task.pause-task", "command": "application:pause-task" },
|
||||
{ "id": "task.resume-task", "command": "application:resume-task" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"type": "button", "icon": "new-task", "id": "task.new-task", "command": "application:new-task", "command-arg": "uri", "command-after": "application:show,index"
|
||||
"type": "button", "icon": "new-task", "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index"
|
||||
},
|
||||
{
|
||||
"type": "spacer", "size": "small"
|
||||
@@ -10,13 +10,13 @@
|
||||
"id": "task.task-list",
|
||||
"items": [
|
||||
{
|
||||
"type": "button", "icon": "task-active", "command": "application:task-list", "command-arg": "active"
|
||||
"type": "button", "icon": "task-active", "command": "application:task-list", "command-arg": { "status": "active" }
|
||||
},
|
||||
{
|
||||
"type": "button", "icon": "task-waiting", "command": "application:task-list", "command-arg": "waiting"
|
||||
"type": "button", "icon": "task-waiting", "command": "application:task-list", "command-arg": { "status": "waiting" }
|
||||
},
|
||||
{
|
||||
"type": "button", "icon": "task-stopped", "command": "application:task-list", "command-arg": "stopped"
|
||||
"type": "button", "icon": "task-stopped", "command": "application:task-list", "command-arg": { "status": "stopped" }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -30,6 +30,6 @@
|
||||
"type": "spacer", "size": "small"
|
||||
},
|
||||
{
|
||||
"type": "button", "icon": "about", "id": "app.about", "command": "application:about", "command-before": "application:show,index"
|
||||
"type": "button", "icon": "about", "id": "app.about", "command": "application:about", "command-before": "application:show?page=index"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": { "type": "torrent" }, "command-after": "application:show?page=index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
|
||||
{ "id": "help.manual", "command": "help:manual" },
|
||||
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.preferences", "command": "application:preferences", "command-before": "application:show,index" },
|
||||
{ "id": "app.preferences", "command": "application:preferences", "command-before": "application:show?page=index" },
|
||||
{ "id": "app.quit", "command": "application:quit" }
|
||||
]
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
{
|
||||
"id": "menu.file",
|
||||
"submenu": [
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
|
||||
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.preferences", "command": "application:preferences" },
|
||||
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
|
||||
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
|
||||
{ "type": "separator" },
|
||||
{ "id": "app.quit", "command": "application:quit" }
|
||||
{ "id": "app.quit", "role": "quit" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "menu.task",
|
||||
"submenu": [
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
|
||||
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
|
||||
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
|
||||
{ "type": "separator" },
|
||||
{ "id": "task.pause-task", "command": "application:pause-task" },
|
||||
{ "id": "task.resume-task", "command": "application:resume-task" },
|
||||
|
||||
+38
-12
@@ -2,6 +2,8 @@ import is from 'electron-is'
|
||||
import { EventEmitter } from 'events'
|
||||
import { app } from 'electron'
|
||||
|
||||
import { bytesToSize } from '@shared/utils'
|
||||
|
||||
import {
|
||||
APP_RUN_MODE
|
||||
} from '@shared/constants'
|
||||
@@ -18,19 +20,43 @@ export default class DockManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
show = isMac ? () => {
|
||||
return app.dock.show()
|
||||
} : () => {}
|
||||
show = isMac
|
||||
? () => {
|
||||
if (app.dock.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
hide = isMac ? () => {
|
||||
app.dock.hide()
|
||||
} : () => {}
|
||||
return app.dock.show()
|
||||
}
|
||||
: () => {}
|
||||
|
||||
setBadge = isMac ? (text) => {
|
||||
app.dock.setBadge(text)
|
||||
} : (text) => {}
|
||||
hide = isMac
|
||||
? () => {
|
||||
if (!app.dock.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
openDock = isMac ? (path) => {
|
||||
app.dock.downloadFinished(path)
|
||||
} : (path) => {}
|
||||
app.dock.hide()
|
||||
}
|
||||
: () => {}
|
||||
|
||||
setBadge = isMac
|
||||
? (text) => {
|
||||
app.dock.setBadge(text)
|
||||
}
|
||||
: (text) => {}
|
||||
|
||||
handleSpeedChange = isMac
|
||||
? (speed) => {
|
||||
const { downloadSpeed } = speed
|
||||
const text = downloadSpeed > 0 ? `${bytesToSize(downloadSpeed)}/s` : ''
|
||||
this.setBadge(text)
|
||||
}
|
||||
: (text) => {}
|
||||
|
||||
openDock = isMac
|
||||
? (path) => {
|
||||
app.dock.downloadFinished(path)
|
||||
}
|
||||
: (path) => {}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
updateStates
|
||||
} from '../utils/menu'
|
||||
import keymap from '@shared/keymap'
|
||||
import { getI18n } from '@/ui/Locale'
|
||||
import { getI18n } from '../ui/Locale'
|
||||
|
||||
export default class MenuManager extends EventEmitter {
|
||||
constructor (options) {
|
||||
@@ -47,7 +47,7 @@ export default class MenuManager extends EventEmitter {
|
||||
this.items = flattenMenuItems(menu)
|
||||
}
|
||||
|
||||
rebuild () {
|
||||
handleLocaleChange (locale) {
|
||||
this.setup()
|
||||
}
|
||||
|
||||
|
||||
+15
-10
@@ -1,40 +1,45 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { nativeTheme, systemPreferences } from 'electron'
|
||||
|
||||
import is from 'electron-is'
|
||||
|
||||
import { APP_THEME } from '@shared/constants'
|
||||
import { getSystemTheme } from '../utils'
|
||||
|
||||
export default class ThemeManager extends EventEmitter {
|
||||
constructor (options = {}) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
this.systemTheme = getSystemTheme()
|
||||
|
||||
this.handleEvents()
|
||||
}
|
||||
|
||||
getSystemTheme () {
|
||||
let result = APP_THEME.LIGHT
|
||||
if (!is.macOS()) {
|
||||
return result
|
||||
}
|
||||
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
|
||||
return result
|
||||
return this.systemTheme
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
|
||||
nativeTheme.on('updated', () => {
|
||||
const theme = this.getSystemTheme()
|
||||
this.updateAppAppearance(theme)
|
||||
this.emit('system-theme-changed', theme)
|
||||
const theme = getSystemTheme()
|
||||
this.systemTheme = theme
|
||||
console.log('nativeTheme updated===>', theme)
|
||||
this.emit('system-theme-change', theme)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* deprecated
|
||||
* @see https://www.electronjs.org/docs/all#systempreferencessetapplevelappearanceappearance-macos-deprecated
|
||||
*/
|
||||
updateAppAppearance (theme) {
|
||||
if (!is.macOS() || theme !== APP_THEME.LIGHT || theme !== APP_THEME.DARK) {
|
||||
return
|
||||
|
||||
+255
-48
@@ -1,54 +1,145 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { join } from 'path'
|
||||
import { Tray, Menu, nativeTheme } from 'electron'
|
||||
import { Tray, Menu, nativeImage } from 'electron'
|
||||
import is from 'electron-is'
|
||||
|
||||
import { APP_THEME } from '@shared/constants'
|
||||
import { getInverseTheme, getSystemMajorVersion } from '@shared/utils'
|
||||
import { getI18n } from './Locale'
|
||||
import {
|
||||
translateTemplate,
|
||||
flattenMenuItems,
|
||||
updateStates
|
||||
} from '../utils/menu'
|
||||
import { getI18n } from '@/ui/Locale'
|
||||
import { APP_THEME } from '@shared/constants'
|
||||
import { convertArrayBufferToBuffer } from '../utils/index'
|
||||
// import logger from '../core/Logger'
|
||||
|
||||
let tray = null
|
||||
const { platform } = process
|
||||
|
||||
export default class TrayManager extends EventEmitter {
|
||||
constructor (options = {}) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
this.theme = options.theme || APP_THEME.AUTO
|
||||
|
||||
this.systemTheme = options.systemTheme
|
||||
this.inverseSystemTheme = getInverseTheme(this.systemTheme)
|
||||
this.bigSur = platform === 'darwin' && getSystemMajorVersion() >= 20
|
||||
|
||||
this.speedometer = options.speedometer
|
||||
|
||||
this.i18n = getI18n()
|
||||
this.menu = null
|
||||
this.cache = {}
|
||||
|
||||
this.uploadSpeed = 0
|
||||
this.downloadSpeed = 0
|
||||
this.status = false
|
||||
this.focused = false
|
||||
|
||||
this.load()
|
||||
this.init()
|
||||
this.setup()
|
||||
}
|
||||
|
||||
init () {
|
||||
this.loadTemplate()
|
||||
|
||||
this.loadImages()
|
||||
|
||||
this.initTray()
|
||||
|
||||
this.setupMenu()
|
||||
|
||||
this.handleEvents()
|
||||
}
|
||||
|
||||
load () {
|
||||
loadTemplate () {
|
||||
this.template = require('../menus/tray.json')
|
||||
}
|
||||
|
||||
let theme = APP_THEME.LIGHT
|
||||
loadImages () {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
this.loadImagesForMacOS()
|
||||
break
|
||||
case 'win32':
|
||||
this.loadImagesForWindows()
|
||||
break
|
||||
case 'linux':
|
||||
this.loadImagesForLinux()
|
||||
break
|
||||
|
||||
if (is.windows()) {
|
||||
theme = 'colorful'
|
||||
} else if (is.macOS()) {
|
||||
theme = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
|
||||
} else if (is.linux()) {
|
||||
theme = (this.theme === APP_THEME.AUTO) ? APP_THEME.DARK : this.theme
|
||||
default:
|
||||
this.loadImagesForDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadImagesForMacOS () {
|
||||
if (this.bigSur) {
|
||||
const {
|
||||
systemTheme,
|
||||
inverseSystemTheme
|
||||
} = this
|
||||
|
||||
this.normalIcon = this.getFromCacheOrCreateImage(`mo-tray-${systemTheme}-normal.png`)
|
||||
this.activeIcon = this.getFromCacheOrCreateImage(`mo-tray-${systemTheme}-active.png`)
|
||||
|
||||
// if (systemTheme === APP_THEME.DARK) {
|
||||
// this.inverseNormalIcon = this.normalIcon
|
||||
// this.inverseActiveIcon = this.activeIcon
|
||||
// } else {
|
||||
this.inverseNormalIcon = this.getFromCacheOrCreateImage(`mo-tray-${inverseSystemTheme}-normal.png`)
|
||||
this.inverseActiveIcon = this.getFromCacheOrCreateImage(`mo-tray-${inverseSystemTheme}-active.png`)
|
||||
// }
|
||||
} else {
|
||||
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
|
||||
}
|
||||
}
|
||||
|
||||
loadImagesForWindows () {
|
||||
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-normal.png')
|
||||
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-active.png')
|
||||
}
|
||||
|
||||
loadImagesForLinux () {
|
||||
const { theme } = this
|
||||
if (theme === APP_THEME.AUTO) {
|
||||
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-dark-normal.png')
|
||||
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-dark-active.png')
|
||||
} else {
|
||||
this.normalIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-normal.png`)
|
||||
this.activeIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-active.png`)
|
||||
}
|
||||
}
|
||||
|
||||
loadImagesForDefault () {
|
||||
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
|
||||
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-light-active.png')
|
||||
}
|
||||
|
||||
getFromCacheOrCreateImage (key) {
|
||||
let file = this.getCache(key)
|
||||
if (file) {
|
||||
return file
|
||||
}
|
||||
|
||||
this.setIcons(theme)
|
||||
file = nativeImage.createFromPath(join(__static, `./${key}`))
|
||||
file.setTemplateImage(this.bigSur)
|
||||
this.setCache(key, file)
|
||||
return file
|
||||
}
|
||||
|
||||
setIcons (theme) {
|
||||
this.normalIcon = join(__static, `./mo-tray-${theme}-normal.png`)
|
||||
this.activeIcon = join(__static, `./mo-tray-${theme}-active.png`)
|
||||
getCache (key) {
|
||||
return this.cache[key]
|
||||
}
|
||||
|
||||
build () {
|
||||
setCache (key, value) {
|
||||
this.cache[key] = value
|
||||
}
|
||||
|
||||
buildMenu () {
|
||||
const keystrokesByCommand = {}
|
||||
for (const item in this.keymap) {
|
||||
keystrokesByCommand[this.keymap[item]] = item
|
||||
@@ -61,73 +152,129 @@ export default class TrayManager extends EventEmitter {
|
||||
this.items = flattenMenuItems(this.menu)
|
||||
}
|
||||
|
||||
setup () {
|
||||
this.build()
|
||||
setupMenu () {
|
||||
this.buildMenu()
|
||||
|
||||
/**
|
||||
* Linux requires setContextMenu to be called
|
||||
* in order for the context menu to populate correctly
|
||||
*/
|
||||
if (process.platform === 'linux') {
|
||||
tray.setContextMenu(this.menu)
|
||||
}
|
||||
this.updateContextMenu()
|
||||
}
|
||||
|
||||
init () {
|
||||
tray = new Tray(this.normalIcon)
|
||||
initTray () {
|
||||
const { icon } = this.getIcons()
|
||||
tray = new Tray(icon)
|
||||
// tray.setPressedImage(inverseIcon)
|
||||
|
||||
tray.setToolTip('Motrix')
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
// All OS
|
||||
tray.on('click', this.handleTrayClick)
|
||||
tray.on('double-click', this.handleTrayDbClick)
|
||||
tray.on('right-click', this.handleTrayRightClick)
|
||||
|
||||
tray.on('drop-files', this.handleTrayDropFile)
|
||||
// macOS, Windows
|
||||
// tray.on('double-click', this.handleTrayDbClick)
|
||||
tray.on('right-click', this.handleTrayRightClick)
|
||||
tray.on('mouse-down', this.handleTrayMouseDown)
|
||||
tray.on('mouse-up', this.handleTrayMouseUp)
|
||||
|
||||
// macOS only
|
||||
tray.setIgnoreDoubleClickEvents(true)
|
||||
tray.on('drop-files', this.handleTrayDropFiles)
|
||||
tray.on('drop-text', this.handleTrayDropText)
|
||||
}
|
||||
|
||||
handleTrayClick = (event) => {
|
||||
event.preventDefault()
|
||||
global.application.toggle()
|
||||
}
|
||||
|
||||
handleTrayDbClick = (event) => {
|
||||
event.preventDefault()
|
||||
global.application.show()
|
||||
}
|
||||
|
||||
handleTrayRightClick = (event) => {
|
||||
event.preventDefault()
|
||||
tray.popUpContextMenu(this.menu)
|
||||
}
|
||||
|
||||
handleTrayDropFile = (event, files) => {
|
||||
global.application.show()
|
||||
global.application.handleFile(files[0])
|
||||
handleTrayMouseDown = (event) => {
|
||||
this.focused = true
|
||||
this.emit('mouse-down', {
|
||||
focused: true,
|
||||
theme: this.inverseSystemTheme
|
||||
})
|
||||
this.renderTray()
|
||||
}
|
||||
|
||||
updateTrayByStatus (status) {
|
||||
this.status = status
|
||||
this.updateTray()
|
||||
handleTrayMouseUp = (event) => {
|
||||
this.focused = false
|
||||
this.emit('mouse-up', {
|
||||
focused: false,
|
||||
theme: this.theme
|
||||
})
|
||||
this.renderTray()
|
||||
}
|
||||
|
||||
updateTray () {
|
||||
const icon = this.status ? this.activeIcon : this.normalIcon
|
||||
tray.setImage(icon)
|
||||
handleTrayDropFiles = (event, files) => {
|
||||
this.emit('drop-files', files)
|
||||
}
|
||||
|
||||
changeIconTheme (theme = APP_THEME.LIGHT) {
|
||||
if (!is.macOS()) {
|
||||
handleTrayDropText = (event, text) => {
|
||||
this.emit('drop-text', text)
|
||||
}
|
||||
|
||||
toggleSpeedometer (enabled) {
|
||||
this.speedometer = enabled
|
||||
}
|
||||
|
||||
async renderTray () {
|
||||
if (this.speedometer) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setIcons(theme)
|
||||
const { icon } = this.getIcons()
|
||||
|
||||
this.updateTray()
|
||||
tray.setImage(icon)
|
||||
// tray.setPressedImage(inverseIcon)
|
||||
|
||||
this.updateContextMenu()
|
||||
}
|
||||
|
||||
getIcons () {
|
||||
if (this.bigSur) {
|
||||
return { icon: this.normalIcon }
|
||||
}
|
||||
|
||||
const { focused, status, systemTheme } = this
|
||||
|
||||
const icon = status ? this.activeIcon : this.normalIcon
|
||||
if (systemTheme === APP_THEME.DARK) {
|
||||
return {
|
||||
icon
|
||||
}
|
||||
}
|
||||
|
||||
const inverseIcon = status ? this.inverseActiveIcon : this.inverseNormalIcon
|
||||
|
||||
return {
|
||||
icon: focused ? inverseIcon : icon
|
||||
// inverseIcon: focused ? icon : inverseIcon
|
||||
}
|
||||
}
|
||||
|
||||
updateContextMenu () {
|
||||
/**
|
||||
* Linux requires setContextMenu to be called
|
||||
* in order for the context menu to populate correctly
|
||||
*/
|
||||
if (process.platform !== 'linux') {
|
||||
return
|
||||
}
|
||||
|
||||
tray.setContextMenu(this.menu)
|
||||
}
|
||||
|
||||
updateMenuStates (visibleStates, enabledStates, checkedStates) {
|
||||
updateStates(this.items, visibleStates, enabledStates, checkedStates)
|
||||
|
||||
this.updateContextMenu()
|
||||
}
|
||||
|
||||
updateMenuItemVisibleState (id, flag) {
|
||||
@@ -144,7 +291,67 @@ export default class TrayManager extends EventEmitter {
|
||||
this.updateMenuStates(null, enabledStates, null)
|
||||
}
|
||||
|
||||
handleLocaleChange (locale) {
|
||||
this.setupMenu()
|
||||
}
|
||||
|
||||
handleSpeedometerEnableChange (enabled) {
|
||||
this.toggleSpeedometer(enabled)
|
||||
|
||||
this.renderTray()
|
||||
}
|
||||
|
||||
handleSystemThemeChange (systemTheme = APP_THEME.LIGHT) {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.systemTheme = systemTheme
|
||||
this.inverseSystemTheme = getInverseTheme(systemTheme)
|
||||
|
||||
this.loadImages()
|
||||
|
||||
this.renderTray()
|
||||
}
|
||||
|
||||
handleDownloadStatusChange (status) {
|
||||
this.status = status
|
||||
|
||||
this.renderTray()
|
||||
}
|
||||
|
||||
async handleSpeedChange ({ uploadSpeed, downloadSpeed }) {
|
||||
if (!this.speedometer) {
|
||||
return
|
||||
}
|
||||
|
||||
this.uploadSpeed = uploadSpeed
|
||||
this.downloadSpeed = downloadSpeed
|
||||
|
||||
await this.renderTray()
|
||||
}
|
||||
|
||||
async updateTrayByImage (ab) {
|
||||
const buffer = convertArrayBufferToBuffer(ab)
|
||||
const image = nativeImage.createFromBuffer(buffer, {
|
||||
scaleFactor: 2
|
||||
})
|
||||
image.setTemplateImage(this.bigSur)
|
||||
tray.setImage(image)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (tray) {
|
||||
tray.removeListener('click', this.handleTrayClick)
|
||||
// tray.removeListener('double-click', this.handleTrayDbClick)
|
||||
tray.removeListener('right-click', this.handleTrayRightClick)
|
||||
tray.removeListener('mouse-down', this.handleTrayMouseDown)
|
||||
tray.removeListener('mouse-up', this.handleTrayMouseUp)
|
||||
|
||||
tray.removeListener('drop-files', this.handleTrayDropFiles)
|
||||
tray.removeListener('drop-text', this.handleTrayDropText)
|
||||
}
|
||||
|
||||
tray.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ const defaultBrowserOptions = {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
nodeIntegration: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +81,14 @@ export default class WindowManager extends EventEmitter {
|
||||
|
||||
window = new BrowserWindow({
|
||||
...defaultBrowserOptions,
|
||||
...pageOptions.attrs
|
||||
...pageOptions.attrs,
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true
|
||||
},
|
||||
hasShadow: !is.macOS()
|
||||
})
|
||||
|
||||
const bounds = this.getPageBounds(page)
|
||||
@@ -105,6 +111,14 @@ export default class WindowManager extends EventEmitter {
|
||||
}
|
||||
})
|
||||
|
||||
window.on('enter-full-screen', () => {
|
||||
this.emit('enter-full-screen', window)
|
||||
})
|
||||
|
||||
window.on('leave-full-screen', () => {
|
||||
this.emit('leave-full-screen', window)
|
||||
})
|
||||
|
||||
this.handleWindowState(page, window)
|
||||
|
||||
this.handleWindowClose(pageOptions, page, window)
|
||||
@@ -169,7 +183,15 @@ export default class WindowManager extends EventEmitter {
|
||||
window.on('close', (event) => {
|
||||
if (pageOptions.bindCloseToHide && !this.willQuit) {
|
||||
event.preventDefault()
|
||||
window.hide()
|
||||
|
||||
// @see https://github.com/electron/electron/issues/20263
|
||||
if (window.isFullScreen()) {
|
||||
window.once('leave-full-screen', () => window.hide())
|
||||
|
||||
window.setFullScreen(false)
|
||||
} else {
|
||||
window.hide()
|
||||
}
|
||||
}
|
||||
const bounds = window.getBounds()
|
||||
this.emit('window-closed', { page, bounds })
|
||||
@@ -178,15 +200,16 @@ export default class WindowManager extends EventEmitter {
|
||||
|
||||
showWindow (page) {
|
||||
const window = this.getWindow(page)
|
||||
if (!window) {
|
||||
if (!window || window.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
window.show()
|
||||
}
|
||||
|
||||
autoHideWindow (page) {
|
||||
hideWindow (page) {
|
||||
const window = this.getWindow(page)
|
||||
if (!window) {
|
||||
if (!window || !window.isVisible()) {
|
||||
return
|
||||
}
|
||||
window.hide()
|
||||
@@ -203,10 +226,11 @@ export default class WindowManager extends EventEmitter {
|
||||
if (!window) {
|
||||
return
|
||||
}
|
||||
if (window.isVisible()) {
|
||||
window.hide()
|
||||
} else {
|
||||
|
||||
if (!window.isVisible() || window.isFullScreen()) {
|
||||
window.show()
|
||||
} else {
|
||||
window.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-7
@@ -1,17 +1,18 @@
|
||||
import { app } from 'electron'
|
||||
import { app, nativeTheme } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { resolve } from 'path'
|
||||
import { existsSync, lstatSync } from 'fs'
|
||||
|
||||
import {
|
||||
APP_THEME,
|
||||
ENGINE_MAX_CONNECTION_PER_SERVER,
|
||||
IP_VERSION
|
||||
} from '@shared/constants'
|
||||
import logger from '../core/Logger'
|
||||
|
||||
import engineBinMap from '../configs/engine'
|
||||
|
||||
export function getLogPath () {
|
||||
return logger.transports.file.file
|
||||
return app.getPath('logs')
|
||||
}
|
||||
|
||||
export function getDhtPath (protocol) {
|
||||
@@ -23,6 +24,10 @@ export function getSessionPath () {
|
||||
return resolve(app.getPath('userData'), './download.session')
|
||||
}
|
||||
|
||||
export function getEnginePidPath () {
|
||||
return resolve(app.getPath('userData'), './engine.pid')
|
||||
}
|
||||
|
||||
export function getUserDataPath () {
|
||||
return app.getPath('userData')
|
||||
}
|
||||
@@ -100,13 +105,13 @@ export function parseArgvAsUrl (argv) {
|
||||
export function checkIsSupportedSchema (url = '') {
|
||||
const str = url.toLowerCase()
|
||||
if (
|
||||
str.startsWith('mo:') ||
|
||||
str.startsWith('motrix:') ||
|
||||
str.startsWith('ftp:') ||
|
||||
str.startsWith('http:') ||
|
||||
str.startsWith('https:') ||
|
||||
str.startsWith('ftp:') ||
|
||||
str.startsWith('magnet:') ||
|
||||
str.startsWith('thunder:')
|
||||
str.startsWith('thunder:') ||
|
||||
str.startsWith('mo:') ||
|
||||
str.startsWith('motrix:')
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
@@ -133,3 +138,18 @@ export function parseArgvAsFile (argv) {
|
||||
export const getMaxConnectionPerServer = () => {
|
||||
return ENGINE_MAX_CONNECTION_PER_SERVER
|
||||
}
|
||||
|
||||
export const getSystemTheme = () => {
|
||||
let result = APP_THEME.LIGHT
|
||||
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
|
||||
return result
|
||||
}
|
||||
|
||||
export const convertArrayBufferToBuffer = (arrayBuffer) => {
|
||||
const buffer = Buffer.alloc(arrayBuffer.byteLength)
|
||||
const view = new Uint8Array(arrayBuffer)
|
||||
for (let i = 0; i < buffer.length; ++i) {
|
||||
buffer[i] = view[i]
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { parse } from 'querystring'
|
||||
|
||||
export function concat (template, submenu, submenuToAdd) {
|
||||
submenuToAdd.forEach(sub => {
|
||||
let relativeItem = null
|
||||
@@ -115,16 +117,18 @@ function handleCommandBefore (item) {
|
||||
if (!item['command-before']) {
|
||||
return
|
||||
}
|
||||
const [command, ...args] = item['command-before'].split(',')
|
||||
global.application.sendCommandToAll(command, ...args)
|
||||
const [command, params] = item['command-before'].split('?')
|
||||
const args = parse(params)
|
||||
global.application.sendCommandToAll(command, args)
|
||||
}
|
||||
|
||||
function handleCommandAfter (item) {
|
||||
if (!item['command-after']) {
|
||||
return
|
||||
}
|
||||
const [command, ...args] = item['command-after'].split(',')
|
||||
global.application.sendCommandToAll(command, ...args)
|
||||
const [command, params] = item['command-after'].split('?')
|
||||
const args = parse(params)
|
||||
global.application.sendCommandToAll(command, args)
|
||||
}
|
||||
|
||||
function acceleratorForCommand (command, keystrokesByCommand) {
|
||||
|
||||
+51
-33
@@ -1,7 +1,7 @@
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import { ipcRenderer } from 'electron'
|
||||
import is from 'electron-is'
|
||||
import { isEmpty } from 'lodash'
|
||||
import Aria2 from 'aria2'
|
||||
import { isEmpty, clone } from 'lodash'
|
||||
import { Aria2 } from '@shared/aria2'
|
||||
import {
|
||||
separateConfig,
|
||||
compactUndefined,
|
||||
@@ -12,19 +12,18 @@ import {
|
||||
} from '@shared/utils'
|
||||
import { ENGINE_RPC_HOST } from '@shared/constants'
|
||||
|
||||
const application = remote.getGlobal('application')
|
||||
|
||||
export default class Api {
|
||||
constructor (options = {}) {
|
||||
this.options = options
|
||||
|
||||
this.client = null
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
this.loadConfig()
|
||||
this.initClient()
|
||||
async init () {
|
||||
this.config = await this.loadConfig()
|
||||
|
||||
this.client = this.initClient()
|
||||
this.client.open()
|
||||
}
|
||||
|
||||
loadConfigFromLocalStorage () {
|
||||
@@ -33,21 +32,18 @@ export default class Api {
|
||||
return result
|
||||
}
|
||||
|
||||
loadConfigFromNativeStore () {
|
||||
const systemConfig = application.configManager.getSystemConfig()
|
||||
const userConfig = application.configManager.getUserConfig()
|
||||
|
||||
const result = { ...systemConfig, ...userConfig }
|
||||
async loadConfigFromNativeStore () {
|
||||
const result = await ipcRenderer.invoke('get-app-config')
|
||||
return result
|
||||
}
|
||||
|
||||
loadConfig () {
|
||||
async loadConfig () {
|
||||
let result = is.renderer()
|
||||
? this.loadConfigFromNativeStore()
|
||||
? await this.loadConfigFromNativeStore()
|
||||
: this.loadConfigFromLocalStorage()
|
||||
|
||||
result = changeKeysToCamelCase(result)
|
||||
this.config = result
|
||||
return result
|
||||
}
|
||||
|
||||
initClient () {
|
||||
@@ -56,12 +52,11 @@ export default class Api {
|
||||
rpcSecret: secret
|
||||
} = this.config
|
||||
const host = ENGINE_RPC_HOST
|
||||
this.client = new Aria2({
|
||||
return new Aria2({
|
||||
host,
|
||||
port,
|
||||
secret
|
||||
})
|
||||
this.client.open()
|
||||
}
|
||||
|
||||
closeClient () {
|
||||
@@ -76,7 +71,7 @@ export default class Api {
|
||||
|
||||
fetchPreference () {
|
||||
return new Promise((resolve) => {
|
||||
this.loadConfig()
|
||||
this.config = this.loadConfig()
|
||||
resolve(this.config)
|
||||
})
|
||||
}
|
||||
@@ -160,11 +155,10 @@ export default class Api {
|
||||
}
|
||||
|
||||
changeOption (params = {}) {
|
||||
let { gid, options = {} } = params
|
||||
options = formatOptionsForEngine(options)
|
||||
const { gid, options = {} } = params
|
||||
|
||||
const kebabOptions = changeKeysToKebabCase(options)
|
||||
const args = compactUndefined([gid, kebabOptions])
|
||||
const engineOptions = formatOptionsForEngine(options)
|
||||
const args = compactUndefined([gid, engineOptions])
|
||||
|
||||
return this.client.call('changeOption', ...args)
|
||||
}
|
||||
@@ -180,11 +174,11 @@ export default class Api {
|
||||
options
|
||||
} = params
|
||||
const tasks = uris.map((uri, index) => {
|
||||
const kebabOptions = changeKeysToKebabCase(options)
|
||||
const engineOptions = formatOptionsForEngine(options)
|
||||
if (outs && outs[index]) {
|
||||
kebabOptions.out = outs[index]
|
||||
engineOptions.out = outs[index]
|
||||
}
|
||||
const args = compactUndefined([[uri], kebabOptions])
|
||||
const args = compactUndefined([[uri], engineOptions])
|
||||
return ['aria2.addUri', ...args]
|
||||
})
|
||||
return this.client.multicall(tasks)
|
||||
@@ -195,8 +189,8 @@ export default class Api {
|
||||
torrent,
|
||||
options
|
||||
} = params
|
||||
const kebabOptions = changeKeysToKebabCase(options)
|
||||
const args = compactUndefined([torrent, [], kebabOptions])
|
||||
const engineOptions = formatOptionsForEngine(options)
|
||||
const args = compactUndefined([torrent, [], engineOptions])
|
||||
return this.client.call('addTorrent', ...args)
|
||||
}
|
||||
|
||||
@@ -205,8 +199,8 @@ export default class Api {
|
||||
metalink,
|
||||
options
|
||||
} = params
|
||||
const kebabOptions = changeKeysToKebabCase(options)
|
||||
const args = compactUndefined([metalink, kebabOptions])
|
||||
const engineOptions = formatOptionsForEngine(options)
|
||||
const args = compactUndefined([metalink, engineOptions])
|
||||
return this.client.call('addMetalink', ...args)
|
||||
}
|
||||
|
||||
@@ -261,6 +255,30 @@ export default class Api {
|
||||
return this.client.call('tellStatus', ...args)
|
||||
}
|
||||
|
||||
fetchTaskItemWithPeers (params = {}) {
|
||||
const { gid, keys } = params
|
||||
const statusArgs = compactUndefined([gid, keys])
|
||||
const peersArgs = compactUndefined([gid])
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.multicall([
|
||||
['aria2.tellStatus', ...statusArgs],
|
||||
['aria2.getPeers', ...peersArgs]
|
||||
]).then((data) => {
|
||||
console.log('[Motrix] fetchTaskItemWithPeers:', data)
|
||||
const result = data[0] && data[0][0]
|
||||
const peers = data[1] && data[1][0]
|
||||
result.peers = peers || []
|
||||
console.log('[Motrix] fetchTaskItemWithPeers.result:', result)
|
||||
console.log('[Motrix] fetchTaskItemWithPeers.peers:', peers)
|
||||
|
||||
resolve(result)
|
||||
}).catch((err) => {
|
||||
console.log('[Motrix] fetch downloading task list fail:', err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fetchTaskItemPeers (params = {}) {
|
||||
const { gid, keys } = params
|
||||
const args = compactUndefined([gid, keys])
|
||||
@@ -328,8 +346,8 @@ export default class Api {
|
||||
options = formatOptionsForEngine(options)
|
||||
|
||||
const data = gids.map((gid, index) => {
|
||||
const kebabOptions = changeKeysToKebabCase(options)
|
||||
const args = compactUndefined([gid, kebabOptions])
|
||||
const _options = clone(options)
|
||||
const args = compactUndefined([gid, _options])
|
||||
return [method, ...args]
|
||||
})
|
||||
return this.client.multicall(data)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g stroke-linecap="square" stroke-linejoin="miter" stroke-width="2" fill="#000000" stroke="#000000">
|
||||
<line fill="none" stroke-miterlimit="10" x1="8.2" y1="4.5" x2="11.1" y2="7.3"></line>
|
||||
<line fill="none" stroke-miterlimit="10" x1="16.7" y1="12.9" x2="19.5" y2="15.8"></line>
|
||||
<path fill="none" stroke="#000000" stroke-miterlimit="10"
|
||||
d="M12.5,17.2 c-1.6,1.6-4.1,1.6-5.7,0c-1.6-1.6-1.6-4.1,0-5.7l7.1-7.1l-2.8-2.8L4,8.7C0.9,11.8,0.9,16.9,4,20s8.2,3.1,11.3,0l7.1-7.1l-2.8-2.8 L12.5,17.2z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
@@ -15,6 +15,7 @@
|
||||
import { mapState } from 'vuex'
|
||||
import AppInfo from '@/components/About/AppInfo'
|
||||
import Copyright from '@/components/About/Copyright'
|
||||
import { app } from '@electron/remote'
|
||||
|
||||
export default {
|
||||
name: 'mo-about-panel',
|
||||
@@ -29,7 +30,7 @@
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const version = this.$electron.remote.app.getVersion()
|
||||
const version = app.getVersion()
|
||||
return {
|
||||
version
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-row class="copyright">
|
||||
<el-col :span="6" class="copyright-left">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/">
|
||||
©2019 Motrix
|
||||
©2021 Motrix
|
||||
</a>
|
||||
</el-col>
|
||||
<el-col :span="18" class="copyright-right">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
...mapState('app', {
|
||||
currentPage: state => state.currentPage
|
||||
}),
|
||||
asideDraggable: function () {
|
||||
asideDraggable () {
|
||||
return is.macOS()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
<template>
|
||||
<div ref="webviewViewport" class="webview-viewport">
|
||||
<webview
|
||||
<iframe
|
||||
class="mo-webview"
|
||||
ref="webview"
|
||||
ref="iframe"
|
||||
:src="src"
|
||||
></webview>
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import { webContents } from '@electron/remote'
|
||||
import { Loading } from 'element-ui'
|
||||
|
||||
import {
|
||||
openExternal
|
||||
} from '@/components/Native/utils'
|
||||
|
||||
export default {
|
||||
name: 'mo-browser',
|
||||
components: {
|
||||
@@ -35,11 +32,11 @@
|
||||
isRenderer: () => is.renderer()
|
||||
},
|
||||
mounted () {
|
||||
const { webview } = this.$refs
|
||||
const { iframe } = this.$refs
|
||||
|
||||
webview.addEventListener('did-start-loading', this.loadStart.bind(this))
|
||||
webview.addEventListener('did-stop-loading', this.loadStop.bind(this))
|
||||
webview.addEventListener('dom-ready', this.ready.bind(this))
|
||||
iframe.addEventListener('did-start-loading', this.loadStart.bind(this))
|
||||
iframe.addEventListener('did-stop-loading', this.loadStop.bind(this))
|
||||
iframe.addEventListener('dom-ready', this.ready.bind(this))
|
||||
},
|
||||
methods: {
|
||||
loadStart () {
|
||||
@@ -54,12 +51,12 @@
|
||||
})
|
||||
},
|
||||
ready () {
|
||||
const { webview } = this.$refs
|
||||
const { iframe } = this.$refs
|
||||
|
||||
const webContents = this.$electron.remote.webContents.fromId(webview.getWebContentsId())
|
||||
webContents.on('new-window', (event, url) => {
|
||||
const wc = webContents.fromId(iframe.getWebContentsId())
|
||||
wc.setWindowOpenHandler((event, url) => {
|
||||
event.preventDefault()
|
||||
openExternal(url)
|
||||
this.$electron.ipcRenderer.send('command', 'application:open-external', url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { Message } from 'element-ui'
|
||||
import { base64StringToBlob } from 'blob-util'
|
||||
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { buildFileList } from '@shared/utils'
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import { getLocaleManager } from '@/components/Locale'
|
||||
import CommandManager from './CommandManager'
|
||||
|
||||
const commands = new CommandManager()
|
||||
const i18n = getLocaleManager().getI18n()
|
||||
|
||||
function updateSystemTheme (theme) {
|
||||
store.dispatch('app/updateSystemTheme', theme)
|
||||
}
|
||||
|
||||
function updateTheme (theme) {
|
||||
store.dispatch('preference/changeThemeConfig', theme)
|
||||
}
|
||||
|
||||
function showAboutPanel () {
|
||||
store.dispatch('app/showAboutPanel')
|
||||
}
|
||||
|
||||
function showAddTask (taskType = ADD_TASK_TYPE.URI, task = '') {
|
||||
if (taskType === ADD_TASK_TYPE.URI && task) {
|
||||
store.dispatch('app/updateAddTaskUrl', task)
|
||||
}
|
||||
store.dispatch('app/showAddTaskDialog', taskType)
|
||||
}
|
||||
|
||||
function showAddBtTask () {
|
||||
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
|
||||
}
|
||||
|
||||
function showAddBtTaskWithFile (fileName, base64Data = '') {
|
||||
const blob = base64StringToBlob(base64Data, 'application/x-bittorrent')
|
||||
const file = new File([blob], fileName, { type: 'application/x-bittorrent' })
|
||||
const fileList = buildFileList(file)
|
||||
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
|
||||
setTimeout(() => {
|
||||
store.dispatch('app/addTaskAddTorrents', { fileList })
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function navigateTaskList (status = 'active') {
|
||||
router.push({ path: `/task/${status}` }).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
function navigatePreferences () {
|
||||
router.push({ path: '/preference' }).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
function showUnderDevelopmentMessage () {
|
||||
Message.info(i18n.t('app.under-development-message'))
|
||||
}
|
||||
|
||||
function pauseTask () {
|
||||
store.dispatch('task/batchPauseSelectedTasks')
|
||||
}
|
||||
|
||||
function resumeTask () {
|
||||
store.dispatch('task/batchResumeSelectedTasks')
|
||||
}
|
||||
|
||||
function deleteTask () {
|
||||
showUnderDevelopmentMessage()
|
||||
}
|
||||
|
||||
function moveTaskUp () {
|
||||
showUnderDevelopmentMessage()
|
||||
}
|
||||
|
||||
function moveTaskDown () {
|
||||
showUnderDevelopmentMessage()
|
||||
}
|
||||
|
||||
function pauseAllTask () {
|
||||
store.dispatch('task/pauseAllTask')
|
||||
}
|
||||
|
||||
function resumeAllTask () {
|
||||
store.dispatch('task/resumeAllTask')
|
||||
}
|
||||
|
||||
function selectAllTask () {
|
||||
store.dispatch('task/selectAllTask')
|
||||
}
|
||||
|
||||
commands.register('application:system-theme', updateSystemTheme)
|
||||
commands.register('application:theme', updateTheme)
|
||||
commands.register('application:about', showAboutPanel)
|
||||
commands.register('application:new-task', showAddTask)
|
||||
commands.register('application:new-bt-task', showAddBtTask)
|
||||
commands.register('application:new-bt-task-with-file', showAddBtTaskWithFile)
|
||||
commands.register('application:task-list', navigateTaskList)
|
||||
commands.register('application:preferences', navigatePreferences)
|
||||
|
||||
commands.register('application:pause-task', pauseTask)
|
||||
commands.register('application:resume-task', resumeTask)
|
||||
commands.register('application:delete-task', deleteTask)
|
||||
commands.register('application:move-task-up', moveTaskUp)
|
||||
commands.register('application:move-task-down', moveTaskDown)
|
||||
commands.register('application:pause-all-task', pauseAllTask)
|
||||
commands.register('application:resume-all-task', resumeAllTask)
|
||||
commands.register('application:select-all-task', selectAllTask)
|
||||
|
||||
export {
|
||||
commands
|
||||
}
|
||||
+11
-3
@@ -9,11 +9,11 @@ export default class CommandManager extends EventEmitter {
|
||||
|
||||
register (id, fn) {
|
||||
if (this.commands[id]) {
|
||||
console.log('Attempting to register an already-registered command: ' + id)
|
||||
console.log('[Motrix] Attempting to register an already-registered command: ' + id)
|
||||
return null
|
||||
}
|
||||
if (!id || !fn) {
|
||||
console.error('Attempting to register a command with a missing id, or command function.')
|
||||
console.error('[Motrix] Attempting to register a command with a missing id, or command function.')
|
||||
return null
|
||||
}
|
||||
this.commands[id] = fn
|
||||
@@ -21,8 +21,16 @@ export default class CommandManager extends EventEmitter {
|
||||
this.emit('commandRegistered', id)
|
||||
}
|
||||
|
||||
unregister (id) {
|
||||
if (this.commands[id]) {
|
||||
delete this.commands[id]
|
||||
|
||||
this.emit('commandUnregistered', id)
|
||||
}
|
||||
}
|
||||
|
||||
execute (id, ...args) {
|
||||
var fn = this.commands[id]
|
||||
const fn = this.commands[id]
|
||||
if (fn) {
|
||||
try {
|
||||
this.emit('beforeExecuteCommand', id)
|
||||
@@ -0,0 +1,3 @@
|
||||
import CommandManager from '.'
|
||||
|
||||
export const commands = new CommandManager()
|
||||
@@ -0,0 +1,20 @@
|
||||
import Icon from '@/components/Icons/Icon'
|
||||
|
||||
Icon.register({
|
||||
'magnet': {
|
||||
'width': 24,
|
||||
'height': 24,
|
||||
'raw': `
|
||||
<line fill="none" stroke-miterlimit="10" x1="8.2" y1="4.5" x2="11.1" y2="7.3"></line>
|
||||
<line fill="none" stroke-miterlimit="10" x1="16.7" y1="12.9" x2="19.5" y2="15.8"></line>
|
||||
<path fill="none" stroke-miterlimit="10"
|
||||
d="M12.5,17.2 c-1.6,1.6-4.1,1.6-5.7,0c-1.6-1.6-1.6-4.1,0-5.7l7.1-7.1l-2.8-2.8L4,8.7C0.9,11.8,0.9,16.9,4,20s8.2,3.1,11.3,0l7.1-7.1l-2.8-2.8 L12.5,17.2z">
|
||||
</path>`,
|
||||
'g': {
|
||||
'stroke': 'currentColor',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
'stroke-width': '2'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -37,6 +37,7 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
font-size: 0;
|
||||
color: $--app-logo-color;
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
<mo-speedometer />
|
||||
<mo-add-task :visible="addTaskVisible" :type="addTaskType" />
|
||||
<mo-about-panel :visible="aboutPanelVisible" />
|
||||
<mo-task-item-info :visible="taskItemInfoVisible" :task="currentTaskItem" />
|
||||
<mo-task-detail
|
||||
:visible="taskDetailVisible"
|
||||
:gid="currentTaskGid"
|
||||
:task="currentTaskItem"
|
||||
:files="currentTaskFiles"
|
||||
:peers="currentTaskPeers"
|
||||
/>
|
||||
<mo-dragger />
|
||||
</el-container>
|
||||
</template>
|
||||
@@ -16,7 +22,7 @@
|
||||
import Aside from '@/components/Aside/Index'
|
||||
import Speedometer from '@/components/Speedometer/Speedometer'
|
||||
import AddTask from '@/components/Task/AddTask'
|
||||
import TaskItemInfo from '@/components/Task/TaskItemInfo'
|
||||
import TaskDetail from '@/components/TaskDetail/Index'
|
||||
import Dragger from '@/components/Dragger/Index'
|
||||
|
||||
export default {
|
||||
@@ -26,7 +32,7 @@
|
||||
[Aside.name]: Aside,
|
||||
[Speedometer.name]: Speedometer,
|
||||
[AddTask.name]: AddTask,
|
||||
[TaskItemInfo.name]: TaskItemInfo,
|
||||
[TaskDetail.name]: TaskDetail,
|
||||
[Dragger.name]: Dragger
|
||||
},
|
||||
computed: {
|
||||
@@ -36,8 +42,11 @@
|
||||
addTaskType: state => state.addTaskType
|
||||
}),
|
||||
...mapState('task', {
|
||||
taskItemInfoVisible: state => state.taskItemInfoVisible,
|
||||
currentTaskItem: state => state.currentTaskItem
|
||||
taskDetailVisible: state => state.taskDetailVisible,
|
||||
currentTaskGid: state => state.currentTaskGid,
|
||||
currentTaskItem: state => state.currentTaskItem,
|
||||
currentTaskFiles: state => state.currentTaskFiles,
|
||||
currentTaskPeers: state => state.currentTaskPeers
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div style="display: none;">
|
||||
<img
|
||||
id="tray-icon-light-normal"
|
||||
src="static/mo-tray-light-normal@2x.png"
|
||||
>
|
||||
<img
|
||||
id="tray-icon-light-active"
|
||||
src="static/mo-tray-light-active@2x.png"
|
||||
>
|
||||
<img
|
||||
id="tray-icon-dark-normal"
|
||||
src="static/mo-tray-dark-normal@2x.png"
|
||||
>
|
||||
<img
|
||||
id="tray-icon-dark-active"
|
||||
src="static/mo-tray-dark-active@2x.png"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { getInverseTheme } from '@shared/utils'
|
||||
import { APP_THEME } from '@shared/constants'
|
||||
|
||||
const cache = {}
|
||||
|
||||
export default {
|
||||
name: 'mo-dynamic-tray',
|
||||
computed: {
|
||||
...mapState('app', {
|
||||
bigSur: state => state.bigSur,
|
||||
iconStatus: state => state.stat.numActive > 0 ? 'active' : 'normal',
|
||||
theme: state => state.systemTheme,
|
||||
focused: state => state.trayFocused,
|
||||
uploadSpeed: state => state.stat.uploadSpeed,
|
||||
downloadSpeed: state => state.stat.downloadSpeed,
|
||||
speed: state => state.stat.uploadSpeed + state.stat.downloadSpeed
|
||||
}),
|
||||
scale () {
|
||||
return 2
|
||||
},
|
||||
currentTheme () {
|
||||
const { theme, focused } = this
|
||||
if (theme === APP_THEME.DARK) {
|
||||
return theme
|
||||
}
|
||||
|
||||
return focused ? getInverseTheme(theme) : theme
|
||||
},
|
||||
iconKey () {
|
||||
const { bigSur, iconStatus, currentTheme } = this
|
||||
return bigSur ? 'tray-icon-light-normal' : `tray-icon-${currentTheme}-${iconStatus}`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async speed (val) {
|
||||
await this.drawTray()
|
||||
},
|
||||
async iconKey (val) {
|
||||
await this.drawTray()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(async () => {
|
||||
await this.drawTray()
|
||||
}, 200)
|
||||
},
|
||||
methods: {
|
||||
async getIcon (key) {
|
||||
if (cache[key]) {
|
||||
return cache[key]
|
||||
}
|
||||
|
||||
const iconImage = document.getElementById(key)
|
||||
const result = await createImageBitmap(iconImage)
|
||||
cache[key] = result
|
||||
|
||||
return result
|
||||
},
|
||||
async drawTray () {
|
||||
const {
|
||||
currentTheme: theme,
|
||||
uploadSpeed,
|
||||
downloadSpeed,
|
||||
scale,
|
||||
iconKey
|
||||
} = this
|
||||
|
||||
const icon = await this.getIcon(iconKey)
|
||||
|
||||
global.app.trayWorker.postMessage({
|
||||
type: 'tray:draw',
|
||||
payload: {
|
||||
theme,
|
||||
icon,
|
||||
uploadSpeed,
|
||||
downloadSpeed,
|
||||
scale
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,44 +7,44 @@
|
||||
import { mapState } from 'vuex'
|
||||
import api from '@/api'
|
||||
import {
|
||||
showItemInFolder,
|
||||
addToRecentTask
|
||||
} from '@/components/Native/utils'
|
||||
import {
|
||||
bytesToSize,
|
||||
getTaskName,
|
||||
getTaskFullPath
|
||||
} from '@shared/utils'
|
||||
getTaskFullPath,
|
||||
showItemInFolder
|
||||
} from '@/utils/native'
|
||||
import { checkTaskIsBT, getTaskName } from '@shared/utils'
|
||||
|
||||
export default {
|
||||
name: 'mo-engine-client',
|
||||
data: function () {
|
||||
return {
|
||||
downloading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer: () => is.renderer(),
|
||||
...mapState('app', {
|
||||
uploadSpeed: state => state.stat.uploadSpeed,
|
||||
downloadSpeed: state => state.stat.downloadSpeed,
|
||||
speed: state => state.stat.uploadSpeed + state.stat.downloadSpeed,
|
||||
interval: state => state.interval,
|
||||
numActive: state => state.stat.numActive
|
||||
downloading: state => state.stat.numActive > 0
|
||||
}),
|
||||
...mapState('task', {
|
||||
taskItemInfoVisible: state => state.taskItemInfoVisible,
|
||||
messages: state => state.messages,
|
||||
seedingList: state => state.seedingList,
|
||||
taskDetailVisible: state => state.taskDetailVisible,
|
||||
enabledFetchPeers: state => state.enabledFetchPeers,
|
||||
currentTaskGid: state => state.currentTaskGid,
|
||||
currentTaskItem: state => state.currentTaskItem
|
||||
}),
|
||||
...mapState('preference', {
|
||||
taskNotification: state => state.config.taskNotification
|
||||
})
|
||||
}),
|
||||
currentTaskIsBT () {
|
||||
return checkTaskIsBT(this.currentTaskItem)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
downloadSpeed (val, oldVal) {
|
||||
const speed = val > 0 ? `${bytesToSize(val)}/s` : ''
|
||||
this.$electron.ipcRenderer.send('event', 'download-speed-change', speed)
|
||||
},
|
||||
numActive (val, oldVal) {
|
||||
this.downloading = val > 0
|
||||
speed (val) {
|
||||
const { uploadSpeed, downloadSpeed } = this
|
||||
this.$electron.ipcRenderer.send('event', 'speed-change', {
|
||||
uploadSpeed,
|
||||
downloadSpeed
|
||||
})
|
||||
},
|
||||
downloading (val, oldVal) {
|
||||
if (val !== oldVal && this.isRenderer) {
|
||||
@@ -62,8 +62,13 @@
|
||||
onDownloadStart (event) {
|
||||
this.$store.dispatch('task/fetchList')
|
||||
this.$store.dispatch('app/resetInterval')
|
||||
console.log('aria2 onDownloadStart', event)
|
||||
this.$store.dispatch('task/saveSession')
|
||||
const [{ gid }] = event
|
||||
const { seedingList } = this
|
||||
if (seedingList.includes(gid)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchTaskItem({ gid })
|
||||
.then((task) => {
|
||||
const taskName = getTaskName(task)
|
||||
@@ -72,8 +77,12 @@
|
||||
})
|
||||
},
|
||||
onDownloadPause (event) {
|
||||
console.log('aria2 onDownloadPause')
|
||||
const [{ gid }] = event
|
||||
const { seedingList } = this
|
||||
if (seedingList.includes(gid)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchTaskItem({ gid })
|
||||
.then((task) => {
|
||||
const taskName = getTaskName(task)
|
||||
@@ -82,7 +91,6 @@
|
||||
})
|
||||
},
|
||||
onDownloadStop (event) {
|
||||
console.log('aria2 onDownloadStop')
|
||||
const [{ gid }] = event
|
||||
this.fetchTaskItem({ gid })
|
||||
.then((task) => {
|
||||
@@ -99,7 +107,7 @@
|
||||
const { errorCode, errorMessage } = task
|
||||
console.error(`[Motrix] download error gid: ${gid}, #${errorCode}, ${errorMessage}`)
|
||||
const message = this.$t('task.download-error-message', { taskName })
|
||||
const link = `<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/Error#${errorCode}" rel="noopener noreferrer">#${errorCode}</a>`
|
||||
const link = `<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/Error#${errorCode}" rel="noopener noreferrer">${errorCode}</a>`
|
||||
this.$msg({
|
||||
type: 'error',
|
||||
showClose: true,
|
||||
@@ -110,30 +118,35 @@
|
||||
})
|
||||
},
|
||||
onDownloadComplete (event) {
|
||||
console.log('aria2 onDownloadComplete')
|
||||
this.$store.dispatch('task/fetchList')
|
||||
const [{ gid }] = event
|
||||
this.$store.dispatch('task/removeFromSeedingList', gid)
|
||||
|
||||
this.fetchTaskItem({ gid })
|
||||
.then((task) => {
|
||||
this.handleDownloadComplete(task, false)
|
||||
})
|
||||
},
|
||||
onBtDownloadComplete (event) {
|
||||
console.log('aria2 onBtDownloadComplete')
|
||||
this.$store.dispatch('task/fetchList')
|
||||
const [{ gid }] = event
|
||||
const { seedingList } = this
|
||||
if (seedingList.includes(gid)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('task/addToSeedingList', gid)
|
||||
|
||||
this.fetchTaskItem({ gid })
|
||||
.then((task) => {
|
||||
this.handleDownloadComplete(task, true)
|
||||
})
|
||||
},
|
||||
handleDownloadComplete (task, isBT) {
|
||||
this.$store.dispatch('task/saveSession')
|
||||
|
||||
const path = getTaskFullPath(task)
|
||||
|
||||
this.showTaskCompleteNotify(task, isBT, path)
|
||||
|
||||
addToRecentTask(task)
|
||||
|
||||
this.$electron.ipcRenderer.send('event', 'task-download-complete', task, path)
|
||||
},
|
||||
showTaskCompleteNotify (task, isBT, path) {
|
||||
@@ -206,8 +219,12 @@
|
||||
this.$store.dispatch('app/fetchGlobalStat')
|
||||
this.$store.dispatch('task/fetchList')
|
||||
|
||||
if (this.taskItemInfoVisible && this.currentTaskItem) {
|
||||
this.$store.dispatch('task/fetchItem', this.currentTaskItem.gid)
|
||||
if (this.taskDetailVisible && this.currentTaskGid) {
|
||||
if (this.currentTaskIsBT && this.enabledFetchPeers) {
|
||||
this.$store.dispatch('task/fetchItemWithPeers', this.currentTaskGid)
|
||||
} else {
|
||||
this.$store.dispatch('task/fetchItem', this.currentTaskGid)
|
||||
}
|
||||
}
|
||||
},
|
||||
stopPolling () {
|
||||
|
||||
@@ -3,44 +3,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import { mapState } from 'vuex'
|
||||
import {
|
||||
commands
|
||||
} from '@/components/Command/index'
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
|
||||
export default {
|
||||
name: 'mo-ipc',
|
||||
computed: {
|
||||
...mapState('preference', {
|
||||
enableEggFeatures: state => state.config.enableEggFeatures
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
methods: {
|
||||
bindIpcEvents: function () {
|
||||
bindIpcEvents () {
|
||||
this.$electron.ipcRenderer.on('command', (event, command, ...args) => {
|
||||
commands.execute(command, ...args)
|
||||
})
|
||||
},
|
||||
unbindIpcEvents: function () {
|
||||
unbindIpcEvents () {
|
||||
this.$electron.ipcRenderer.removeAllListeners('command')
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created () {
|
||||
this.bindIpcEvents()
|
||||
// id of the menu item
|
||||
const visibleStates = {}
|
||||
if (is.mas()) {
|
||||
visibleStates['app.check-for-updates'] = false
|
||||
if (!this.enableEggFeatures) {
|
||||
visibleStates['task.new-bt-task'] = false
|
||||
}
|
||||
}
|
||||
this.$electron.ipcRenderer.send('command', 'application:change-menu-states', visibleStates, null, null)
|
||||
},
|
||||
destroyed: function () {
|
||||
destroyed () {
|
||||
this.unbindIpcEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dialog } from '@electron/remote'
|
||||
import '@/components/Icons/folder'
|
||||
|
||||
export default {
|
||||
@@ -15,9 +16,9 @@
|
||||
props: {
|
||||
},
|
||||
methods: {
|
||||
onFolderClick: function () {
|
||||
onFolderClick () {
|
||||
const self = this
|
||||
this.$electron.remote.dialog.showOpenDialog({
|
||||
dialog.showOpenDialog({
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
}).then(({ canceled, filePaths }) => {
|
||||
if (canceled || filePaths.length === 0) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import '@/components/Icons/folder'
|
||||
import {
|
||||
showItemInFolder
|
||||
} from '@/components/Native/utils'
|
||||
} from '@/utils/native'
|
||||
|
||||
export default {
|
||||
name: 'mo-show-in-folder',
|
||||
@@ -20,7 +20,7 @@
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
onFolderClick: function () {
|
||||
onFolderClick () {
|
||||
if (!this.path) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCurrentWindow } from '@electron/remote'
|
||||
import '@/components/Icons/win-minimize'
|
||||
import '@/components/Icons/win-maximize'
|
||||
import '@/components/Icons/win-close'
|
||||
@@ -28,22 +29,22 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
win: function () {
|
||||
return this.$electron.remote.getCurrentWindow()
|
||||
win () {
|
||||
return getCurrentWindow()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMinimize: function () {
|
||||
handleMinimize () {
|
||||
this.win.minimize()
|
||||
},
|
||||
handleMaximize: function () {
|
||||
handleMaximize () {
|
||||
if (this.win.isMaximized()) {
|
||||
this.win.unmaximize()
|
||||
} else {
|
||||
this.win.maximize()
|
||||
}
|
||||
},
|
||||
handleClose: function () {
|
||||
handleClose () {
|
||||
this.win.close()
|
||||
}
|
||||
}
|
||||
@@ -73,9 +74,11 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 5100;
|
||||
font-size: 0;
|
||||
> li {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
padding: 5px 18px;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: $--titlebar-actions-color;
|
||||
&:hover {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import is from 'electron-is'
|
||||
import { access, constants } from 'fs'
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
import {
|
||||
isMagnetTask,
|
||||
getTaskFullPath,
|
||||
bytesToSize
|
||||
} from '@shared/utils'
|
||||
import { APP_THEME, TASK_STATUS } from '@shared/constants'
|
||||
|
||||
const remote = is.renderer() ? require('electron').remote : {}
|
||||
|
||||
export function getUserDownloadsPath () {
|
||||
return remote.app.getPath('downloads')
|
||||
}
|
||||
|
||||
export function prettifyDir (dir) {
|
||||
const downloads = getUserDownloadsPath()
|
||||
const result = dir === downloads ? 'Downloads' : dir
|
||||
return result
|
||||
}
|
||||
|
||||
export function showItemInFolder (fullPath, { errorMsg }) {
|
||||
if (!fullPath) {
|
||||
return
|
||||
}
|
||||
|
||||
access(fullPath, constants.F_OK, (err) => {
|
||||
console.log(`${fullPath} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (err) {
|
||||
Message.error(errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
remote.shell.showItemInFolder(fullPath)
|
||||
})
|
||||
}
|
||||
|
||||
export function openItem (fullPath, { errorMsg }) {
|
||||
if (!fullPath) {
|
||||
return
|
||||
}
|
||||
const result = remote.shell.openItem(fullPath)
|
||||
if (!result && errorMsg) {
|
||||
Message.error(errorMsg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function moveTaskFilesToTrash (task) {
|
||||
/**
|
||||
* For magnet link tasks, there is bittorrent, but there is no bittorrent.info.
|
||||
* The path is not a complete path before it becomes a BT task.
|
||||
* In order to avoid accidentally deleting the directory
|
||||
* where the task is located, it directly returns true when deleting.
|
||||
*/
|
||||
if (isMagnetTask(task)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { dir, status } = task
|
||||
const path = getTaskFullPath(task)
|
||||
if (!path || dir === path) {
|
||||
throw new Error('task.file-path-error')
|
||||
}
|
||||
|
||||
let deleteResult1 = true
|
||||
access(path, constants.F_OK, (err) => {
|
||||
console.log(`${path} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (!err) {
|
||||
deleteResult1 = remote.shell.moveItemToTrash(path)
|
||||
}
|
||||
})
|
||||
|
||||
// There is no configuration file for the completed task.
|
||||
if (status === TASK_STATUS.COMPLETE) {
|
||||
return deleteResult1
|
||||
}
|
||||
|
||||
let deleteResult2 = true
|
||||
const extraFilePath = `${path}.aria2`
|
||||
access(extraFilePath, constants.F_OK, (err) => {
|
||||
console.log(`${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (!err) {
|
||||
deleteResult2 = remote.shell.moveItemToTrash(extraFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
return deleteResult1 && deleteResult2
|
||||
}
|
||||
|
||||
export function openDownloadDock (path) {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
remote.app.dock.downloadFinished(path)
|
||||
}
|
||||
|
||||
export function updateDockBadge (text) {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
remote.app.dock.setBadge(text)
|
||||
}
|
||||
|
||||
export function showDownloadSpeedInDock (downloadSpeed) {
|
||||
if (!is.macOS()) {
|
||||
return
|
||||
}
|
||||
const text = downloadSpeed > 0 ? `${bytesToSize(downloadSpeed)}/s` : ''
|
||||
updateDockBadge(text)
|
||||
}
|
||||
|
||||
export function addToRecentTask (task) {
|
||||
if (is.linux()) {
|
||||
return
|
||||
}
|
||||
const path = getTaskFullPath(task)
|
||||
remote.app.addRecentDocument(path)
|
||||
}
|
||||
|
||||
export function addToRecentTaskByPath (path) {
|
||||
if (is.linux()) {
|
||||
return
|
||||
}
|
||||
remote.app.addRecentDocument(path)
|
||||
}
|
||||
|
||||
export function clearRecentTasks () {
|
||||
if (is.linux()) {
|
||||
return
|
||||
}
|
||||
remote.app.clearRecentDocuments()
|
||||
}
|
||||
|
||||
export function getSystemTheme () {
|
||||
let result = APP_THEME.LIGHT
|
||||
if (!is.macOS()) {
|
||||
return result
|
||||
}
|
||||
result = remote.nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
|
||||
return result
|
||||
}
|
||||
|
||||
export const openExternal = (url, options) => {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
remote.shell.openExternal(url, options)
|
||||
}
|
||||
@@ -94,6 +94,8 @@
|
||||
<el-select
|
||||
class="select-track-source"
|
||||
v-model="form.trackerSource"
|
||||
allow-create
|
||||
filterable
|
||||
multiple
|
||||
>
|
||||
<el-option-group
|
||||
@@ -105,7 +107,18 @@
|
||||
v-for="item in group.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
:value="item.value"
|
||||
>
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; margin-right: 24px">
|
||||
<el-tag
|
||||
type="success"
|
||||
size="mini"
|
||||
v-if="item.cdn"
|
||||
>
|
||||
CDN
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
@@ -169,10 +182,10 @@
|
||||
:label-width="formLabelWidth"
|
||||
>
|
||||
<el-row style="margin-bottom: 8px;">
|
||||
<el-col class="form-item-sub" :span="10">
|
||||
<el-col class="form-item-sub" :span="12">
|
||||
<el-switch
|
||||
v-model="form.enableUpnp"
|
||||
active-text="UPnP"
|
||||
active-text="UPnP/NAT-PMP"
|
||||
>
|
||||
</el-switch>
|
||||
</el-col>
|
||||
@@ -292,6 +305,9 @@
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col class="form-item-sub" :span="24">
|
||||
<el-button plain type="warning" @click="() => onSessionResetClick()">
|
||||
{{ $t('preferences.session-reset') }}
|
||||
</el-button>
|
||||
<el-button plain type="danger" @click="() => onFactoryResetClick()">
|
||||
{{ $t('preferences.factory-reset') }}
|
||||
</el-button>
|
||||
@@ -317,6 +333,7 @@
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import { dialog } from '@electron/remote'
|
||||
import { mapState } from 'vuex'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import randomize from 'randomatic'
|
||||
@@ -339,7 +356,7 @@
|
||||
import '@/components/Icons/sync'
|
||||
import '@/components/Icons/refresh'
|
||||
|
||||
const initialForm = (config) => {
|
||||
const initForm = (config) => {
|
||||
const {
|
||||
allProxy,
|
||||
allProxyBackup,
|
||||
@@ -393,7 +410,7 @@
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
const form = initialForm(this.$store.state.preference.config)
|
||||
const form = initForm(this.$store.state.preference.config)
|
||||
const formOriginal = cloneDeep(form)
|
||||
|
||||
return {
|
||||
@@ -407,11 +424,11 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer () { return is.renderer() },
|
||||
isRenderer: () => is.renderer(),
|
||||
title () {
|
||||
return this.$t('preferences.advanced')
|
||||
},
|
||||
subnavs: function () {
|
||||
subnavs () {
|
||||
return [
|
||||
{
|
||||
key: 'basic',
|
||||
@@ -463,8 +480,9 @@
|
||||
const tracker = convertTrackerDataToLine(data)
|
||||
this.form.lastSyncTrackerTime = Date.now()
|
||||
this.form.btTracker = tracker
|
||||
this.trackerSyncing = false
|
||||
})
|
||||
.finally(() => {
|
||||
.catch((_) => {
|
||||
this.trackerSyncing = false
|
||||
})
|
||||
},
|
||||
@@ -505,8 +523,25 @@
|
||||
this.hideRpcSecret = true
|
||||
}, 2000)
|
||||
},
|
||||
onSessionResetClick () {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('preferences.session-reset'),
|
||||
message: this.$t('preferences.session-reset-confirm'),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1
|
||||
}).then(({ response }) => {
|
||||
if (response === 0) {
|
||||
this.$store.dispatch('task/purgeTaskRecord')
|
||||
this.$store.dispatch('task/pauseAllTask')
|
||||
.then(() => {
|
||||
this.$electron.ipcRenderer.send('command', 'application:reset-session')
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onFactoryResetClick () {
|
||||
this.$electron.remote.dialog.showMessageBox({
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('preferences.factory-reset'),
|
||||
message: this.$t('preferences.factory-reset-confirm'),
|
||||
@@ -521,14 +556,14 @@
|
||||
syncFormConfig () {
|
||||
this.$store.dispatch('preference/fetchPreference')
|
||||
.then((config) => {
|
||||
this.form = initialForm(config)
|
||||
this.form = initForm(config)
|
||||
this.formOriginal = cloneDeep(this.form)
|
||||
})
|
||||
},
|
||||
submitForm (formName) {
|
||||
this.$refs[formName].validate((valid) => {
|
||||
if (!valid) {
|
||||
console.log('[Motrix] preference form valid:', valid)
|
||||
console.error('[Motrix] preference form valid:', valid)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -589,6 +624,9 @@
|
||||
.select-track-source {
|
||||
width: 100%;
|
||||
}
|
||||
.el-select__tags {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ua-group {
|
||||
|
||||
@@ -37,8 +37,14 @@
|
||||
{{ $t('preferences.auto-hide-window') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<el-col v-if="isMac" class="form-item-sub" :span="16">
|
||||
<el-checkbox v-model="form.traySpeedometer">
|
||||
{{ $t('preferences.tray-speedometer') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="isMac"
|
||||
:label="`${$t('preferences.run-mode')}: `"
|
||||
:label-width="formLabelWidth"
|
||||
>
|
||||
@@ -137,6 +143,49 @@
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="`${$t('preferences.bt-settings')}: `"
|
||||
:label-width="formLabelWidth"
|
||||
>
|
||||
<el-col class="form-item-sub" :span="24">
|
||||
<el-checkbox v-model="form.btSaveMetadata">
|
||||
{{ $t('preferences.bt-save-metadata') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<el-col class="form-item-sub" :span="24">
|
||||
<el-switch
|
||||
v-model="form.keepSeeding"
|
||||
:active-text="$t('preferences.keep-seeding')"
|
||||
@change="onKeepSeedingChange"
|
||||
>
|
||||
</el-switch>
|
||||
</el-col>
|
||||
<el-col class="form-item-sub" :span="24" v-if="!form.keepSeeding">
|
||||
{{ $t('preferences.seed-ratio') }}
|
||||
<el-input-number
|
||||
v-model="form.seedRatio"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
:label="$t('preferences.seed-ratio')">
|
||||
</el-input-number>
|
||||
</el-col>
|
||||
<el-col class="form-item-sub" :span="24" v-if="!form.keepSeeding">
|
||||
{{ $t('preferences.seed-time') }}
|
||||
({{ $t('preferences.seed-time-unit') }})
|
||||
<el-input-number
|
||||
v-model="form.seedTime"
|
||||
controls-position="right"
|
||||
:min="60"
|
||||
:max="525600"
|
||||
:step="1"
|
||||
:label="$t('preferences.seed-time')">
|
||||
</el-input-number>
|
||||
</el-col>
|
||||
<div class="el-form-item__info" style="margin-top: 8px;">
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="`${$t('preferences.task-manage')}: `"
|
||||
:label-width="formLabelWidth"
|
||||
@@ -176,6 +225,11 @@
|
||||
{{ $t('preferences.task-completed-notify') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<el-col class="form-item-sub" :span="24">
|
||||
<el-checkbox v-model="form.noConfirmBeforeDeleteTask">
|
||||
{{ $t('preferences.no-confirm-before-delete-task') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="form-actions">
|
||||
@@ -204,7 +258,6 @@
|
||||
import ThemeSwitcher from '@/components/Preference/ThemeSwitcher'
|
||||
import { availableLanguages, getLanguage } from '@shared/locales'
|
||||
import { getLocaleManager } from '@/components/Locale'
|
||||
import { prettifyDir } from '@/components/Native/utils'
|
||||
import {
|
||||
calcFormLabelWidth,
|
||||
checkIsNeedRestart,
|
||||
@@ -212,12 +265,14 @@
|
||||
} from '@shared/utils'
|
||||
import { APP_RUN_MODE } from '@shared/constants'
|
||||
|
||||
const initialForm = (config) => {
|
||||
const initForm = (config) => {
|
||||
const {
|
||||
autoHideWindow,
|
||||
btSaveMetadata,
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
hideAppMenu,
|
||||
keepSeeding,
|
||||
keepWindowState,
|
||||
locale,
|
||||
maxConcurrentDownloads,
|
||||
@@ -225,18 +280,24 @@
|
||||
maxOverallDownloadLimit,
|
||||
maxOverallUploadLimit,
|
||||
newTaskShowDownloading,
|
||||
noConfirmBeforeDeleteTask,
|
||||
openAtLogin,
|
||||
resumeAllWhenAppLaunched,
|
||||
runMode,
|
||||
seedRatio,
|
||||
seedTime,
|
||||
taskNotification,
|
||||
theme
|
||||
theme,
|
||||
traySpeedometer
|
||||
} = config
|
||||
const result = {
|
||||
autoHideWindow,
|
||||
btSaveMetadata,
|
||||
continue: config.continue,
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
hideAppMenu,
|
||||
keepSeeding,
|
||||
keepWindowState,
|
||||
locale,
|
||||
maxConcurrentDownloads,
|
||||
@@ -244,11 +305,15 @@
|
||||
maxOverallDownloadLimit,
|
||||
maxOverallUploadLimit,
|
||||
newTaskShowDownloading,
|
||||
noConfirmBeforeDeleteTask,
|
||||
openAtLogin,
|
||||
resumeAllWhenAppLaunched,
|
||||
runMode,
|
||||
seedRatio,
|
||||
seedTime,
|
||||
taskNotification,
|
||||
theme
|
||||
theme,
|
||||
traySpeedometer
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -262,7 +327,7 @@
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
const form = initialForm(this.$store.state.preference.config)
|
||||
const form = initForm(this.$store.state.preference.config)
|
||||
const formOriginal = cloneDeep(form)
|
||||
|
||||
return {
|
||||
@@ -274,8 +339,9 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer () { return is.renderer() },
|
||||
isMas () { return is.mas() },
|
||||
isRenderer: () => is.renderer(),
|
||||
isMac: () => is.macOS(),
|
||||
isMas: () => is.mas(),
|
||||
isLinux () { return is.linux() },
|
||||
title () {
|
||||
return this.$t('preferences.basic')
|
||||
@@ -314,6 +380,14 @@
|
||||
label: '1 MB/s',
|
||||
value: '1M'
|
||||
},
|
||||
{
|
||||
label: '2 MB/s',
|
||||
value: '2M'
|
||||
},
|
||||
{
|
||||
label: '3 MB/s',
|
||||
value: '3M'
|
||||
},
|
||||
{
|
||||
label: '5 MB/s',
|
||||
value: '5M'
|
||||
@@ -321,10 +395,14 @@
|
||||
{
|
||||
label: '10 MB/s',
|
||||
value: '10M'
|
||||
},
|
||||
{
|
||||
label: '20 MB/s',
|
||||
value: '20M'
|
||||
}
|
||||
]
|
||||
},
|
||||
subnavs: function () {
|
||||
subnavs () {
|
||||
return [
|
||||
{
|
||||
key: 'basic',
|
||||
@@ -346,9 +424,6 @@
|
||||
showHideAppMenuOption () {
|
||||
return is.windows() || is.linux()
|
||||
},
|
||||
downloadDir () {
|
||||
return prettifyDir(this.form.dir)
|
||||
},
|
||||
...mapState('preference', {
|
||||
config: state => state.config
|
||||
})
|
||||
@@ -365,20 +440,24 @@
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:change-theme', theme)
|
||||
},
|
||||
onKeepSeedingChange (enable) {
|
||||
this.form.seedRatio = enable ? 0 : 1
|
||||
this.form.seedTime = enable ? 525600 : 60
|
||||
},
|
||||
onDirectorySelected (dir) {
|
||||
this.form.dir = dir
|
||||
},
|
||||
syncFormConfig () {
|
||||
this.$store.dispatch('preference/fetchPreference')
|
||||
.then((config) => {
|
||||
this.form = initialForm(config)
|
||||
this.form = initForm(config)
|
||||
this.formOriginal = cloneDeep(this.form)
|
||||
})
|
||||
},
|
||||
submitForm (formName) {
|
||||
this.$refs[formName].validate((valid) => {
|
||||
if (!valid) {
|
||||
console.log('[Motrix] preference form valid:', valid)
|
||||
console.error('[Motrix] preference form valid:', valid)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -403,11 +482,15 @@
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:open-at-login', openAtLogin)
|
||||
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:toggle-dock', runMode === APP_RUN_MODE.STANDARD)
|
||||
if ('runMode' in changed) {
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:toggle-dock', runMode === APP_RUN_MODE.STANDARD)
|
||||
}
|
||||
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:auto-hide-window', autoHideWindow)
|
||||
if ('autoHideWindow' in changed) {
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
'application:auto-hide-window', autoHideWindow)
|
||||
}
|
||||
|
||||
if (checkIsNeedRestart(data)) {
|
||||
this.$electron.ipcRenderer.send('command',
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'mo-content-preference',
|
||||
computed: {
|
||||
},
|
||||
components: {
|
||||
},
|
||||
methods: {
|
||||
created () {
|
||||
this.$store.dispatch('preference/fetchPreference')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -59,16 +56,13 @@
|
||||
}
|
||||
}
|
||||
.form-actions {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
z-index: 10;
|
||||
width: -webkit-fill-available;
|
||||
box-sizing: border-box;
|
||||
padding: 24px 36px;
|
||||
margin-left: -36px;
|
||||
// aside.width + subnav.width + padding-left + scrollbar.width
|
||||
margin-right: 322px;
|
||||
padding: 24px 36px 24px 0;
|
||||
}
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
title () {
|
||||
return this.$t('preferences.lab')
|
||||
},
|
||||
subnavs: function () {
|
||||
subnavs () {
|
||||
return [
|
||||
{
|
||||
key: 'basic',
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
default: APP_THEME.AUTO
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
data () {
|
||||
return {
|
||||
currentValue: this.value
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentValue: function (val) {
|
||||
currentValue (val) {
|
||||
this.$emit('change', val)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title: function () {
|
||||
title () {
|
||||
return this.$t('subnav.preferences')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
nav: function (category = 'basic') {
|
||||
nav (category = 'basic') {
|
||||
this.$router.push({
|
||||
path: `/preference/${category}`
|
||||
}).catch(err => {
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title: function () {
|
||||
title () {
|
||||
return this.$t('subnav.task-list')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
nav: function (status = 'active') {
|
||||
nav (status = 'active') {
|
||||
this.$router.push({
|
||||
path: `/task/${status}`
|
||||
}).catch(err => {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
:label-width="formLabelWidth"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="form.maxConnectionPerServer"
|
||||
v-model="form.split"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="config.engineMaxConnectionPerServer"
|
||||
@@ -172,39 +172,15 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import SelectDirectory from '@/components/Native/SelectDirectory'
|
||||
import SelectTorrent from '@/components/Task/SelectTorrent'
|
||||
import { prettifyDir } from '@/components/Native/utils'
|
||||
import { ADD_TASK_TYPE, NONE_SELECTED_FILES, SELECTED_ALL_FILES } from '@shared/constants'
|
||||
import { detectResource, splitTaskLinks } from '@shared/utils'
|
||||
import { buildOuts } from '@shared/utils/rename'
|
||||
import {
|
||||
initTaskForm,
|
||||
buildUriPayload,
|
||||
buildTorrentPayload
|
||||
} from '@/utils/task'
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import { detectResource } from '@shared/utils'
|
||||
import '@/components/Icons/inbox'
|
||||
|
||||
const initialForm = state => {
|
||||
const { addTaskUrl, addTaskOptions } = state.app
|
||||
const {
|
||||
allProxy,
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
maxConnectionPerServer,
|
||||
newTaskShowDownloading
|
||||
} = state.preference.config
|
||||
const result = {
|
||||
allProxy,
|
||||
cookie: '',
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
maxConnectionPerServer,
|
||||
newTaskShowDownloading,
|
||||
out: '',
|
||||
referer: '',
|
||||
selectFile: NONE_SELECTED_FILES,
|
||||
torrent: '',
|
||||
uris: addTaskUrl,
|
||||
userAgent: '',
|
||||
...addTaskOptions
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'mo-add-task',
|
||||
components: {
|
||||
@@ -230,23 +206,20 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer () { return is.renderer() },
|
||||
isMas () { return is.mas() },
|
||||
taskType: function () {
|
||||
return this.type
|
||||
},
|
||||
downloadDir: function () {
|
||||
return prettifyDir(this.form.dir)
|
||||
},
|
||||
isRenderer: () => is.renderer(),
|
||||
isMas: () => is.mas(),
|
||||
...mapState('app', {
|
||||
taskList: state => state.taskList
|
||||
}),
|
||||
...mapState('preference', {
|
||||
config: state => state.config
|
||||
})
|
||||
}),
|
||||
taskType () {
|
||||
return this.type
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
taskType: function (current, previous) {
|
||||
taskType (current, previous) {
|
||||
if (this.visible && previous === ADD_TASK_TYPE.URI) {
|
||||
return
|
||||
}
|
||||
@@ -256,18 +229,16 @@
|
||||
this.$refs.uri && this.$refs.uri.focus()
|
||||
}, 50)
|
||||
}
|
||||
},
|
||||
visible (current) {
|
||||
if (current === true) {
|
||||
document.addEventListener('keydown', this.handleHotkey)
|
||||
} else {
|
||||
document.removeEventListener('keydown', this.handleHotkey)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleOpen () {
|
||||
this.form = initialForm(this.$store.state)
|
||||
if (this.taskType === ADD_TASK_TYPE.URI) {
|
||||
this.autofillResourceLink()
|
||||
setTimeout(() => {
|
||||
this.$refs.uri && this.$refs.uri.focus()
|
||||
}, 50)
|
||||
}
|
||||
},
|
||||
autofillResourceLink () {
|
||||
const content = this.$electron.clipboard.readText()
|
||||
const hasResource = detectResource(content)
|
||||
@@ -278,9 +249,21 @@
|
||||
this.form.uris = content
|
||||
}
|
||||
},
|
||||
handleOpen () {
|
||||
this.form = initTaskForm(this.$store.state)
|
||||
if (this.taskType === ADD_TASK_TYPE.URI) {
|
||||
this.autofillResourceLink()
|
||||
setTimeout(() => {
|
||||
this.$refs.uri && this.$refs.uri.focus()
|
||||
}, 50)
|
||||
}
|
||||
},
|
||||
handleOpened () {
|
||||
this.detectThunderResource(this.form.uris)
|
||||
},
|
||||
handleCancel (formName) {
|
||||
this.$store.dispatch('app/hideAddTaskDialog')
|
||||
},
|
||||
handleClose (done) {
|
||||
this.$store.dispatch('app/hideAddTaskDialog')
|
||||
this.$store.dispatch('app/updateAddTaskOptions', {})
|
||||
@@ -288,6 +271,13 @@
|
||||
handleClosed () {
|
||||
this.reset()
|
||||
},
|
||||
handleHotkey (event) {
|
||||
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault()
|
||||
|
||||
this.submitForm('taskForm')
|
||||
}
|
||||
},
|
||||
handleTabClick (tab, event) {
|
||||
this.$store.dispatch('app/changeAddTaskType', tab.name)
|
||||
},
|
||||
@@ -315,94 +305,17 @@
|
||||
},
|
||||
reset () {
|
||||
this.showAdvanced = false
|
||||
this.form = initialForm(this.$store.state)
|
||||
},
|
||||
handleCancel (formName) {
|
||||
this.$store.dispatch('app/hideAddTaskDialog')
|
||||
},
|
||||
buildHeader (form) {
|
||||
const { userAgent, referer, cookie } = form
|
||||
const result = []
|
||||
|
||||
if (!isEmpty(userAgent)) {
|
||||
result.push(`User-Agent: ${userAgent}`)
|
||||
}
|
||||
if (!isEmpty(referer)) {
|
||||
result.push(`Referer: ${referer}`)
|
||||
}
|
||||
if (!isEmpty(cookie)) {
|
||||
result.push(`Cookie: ${cookie}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
buildOption (type, form) {
|
||||
const { allProxy, dir, out, selectFile } = form
|
||||
const result = {}
|
||||
|
||||
if (!isEmpty(allProxy)) {
|
||||
result.allProxy = allProxy
|
||||
}
|
||||
|
||||
if (!isEmpty(dir)) {
|
||||
result.dir = dir
|
||||
}
|
||||
|
||||
if (!isEmpty(out)) {
|
||||
result.out = out
|
||||
}
|
||||
|
||||
if (type === ADD_TASK_TYPE.TORRENT) {
|
||||
if (
|
||||
selectFile !== SELECTED_ALL_FILES &&
|
||||
selectFile !== NONE_SELECTED_FILES
|
||||
) {
|
||||
result.selectFile = selectFile
|
||||
}
|
||||
}
|
||||
|
||||
const header = this.buildHeader(form)
|
||||
if (!isEmpty(header)) {
|
||||
result.header = header
|
||||
}
|
||||
return result
|
||||
},
|
||||
buildUriPayload (form) {
|
||||
let { uris, out } = form
|
||||
if (isEmpty(uris)) {
|
||||
throw new Error(this.$t('task.new-task-uris-required'))
|
||||
}
|
||||
uris = splitTaskLinks(uris)
|
||||
const outs = buildOuts(uris, out)
|
||||
|
||||
const options = this.buildOption(ADD_TASK_TYPE.URI, form)
|
||||
const result = {
|
||||
uris,
|
||||
outs,
|
||||
options
|
||||
}
|
||||
return result
|
||||
},
|
||||
buildTorrentPayload (form) {
|
||||
const { torrent } = form
|
||||
if (isEmpty(torrent)) {
|
||||
throw new Error(this.$t('task.new-task-torrent-required'))
|
||||
}
|
||||
const options = this.buildOption(ADD_TASK_TYPE.TORRENT, form)
|
||||
const result = {
|
||||
torrent,
|
||||
options
|
||||
}
|
||||
return result
|
||||
this.form = initTaskForm(this.$store.state)
|
||||
},
|
||||
addTask (type, form) {
|
||||
let payload = null
|
||||
if (type === ADD_TASK_TYPE.URI) {
|
||||
payload = this.buildUriPayload(form)
|
||||
payload = buildUriPayload(form)
|
||||
this.$store.dispatch('task/addUri', payload).catch(err => {
|
||||
this.$msg.error(err.message)
|
||||
})
|
||||
} else if (type === ADD_TASK_TYPE.TORRENT) {
|
||||
payload = this.buildTorrentPayload(form)
|
||||
payload = buildTorrentPayload(form)
|
||||
this.$store.dispatch('task/addTorrent', payload).catch(err => {
|
||||
this.$msg.error(err.message)
|
||||
})
|
||||
@@ -430,7 +343,7 @@
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$msg.error(err.message)
|
||||
this.$msg.error(this.$t(err.message))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,10 +33,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dialog } from '@electron/remote'
|
||||
import { mapState } from 'vuex'
|
||||
import * as clipboard from 'clipboard-polyfill'
|
||||
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import TaskSubnav from '@/components/Subnav/TaskSubnav'
|
||||
import TaskActions from '@/components/Task/TaskActions'
|
||||
import TaskList from '@/components/Task/TaskList'
|
||||
import SubnavSwitcher from '@/components/Subnav/SubnavSwitcher'
|
||||
import {
|
||||
getTaskUri,
|
||||
parseHeader
|
||||
} from '@shared/utils'
|
||||
import {
|
||||
delayDeleteTaskFiles,
|
||||
showItemInFolder,
|
||||
moveTaskFilesToTrash
|
||||
} from '@/utils/native'
|
||||
|
||||
export default {
|
||||
name: 'mo-content-task',
|
||||
@@ -53,7 +68,15 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subnavs: function () {
|
||||
...mapState('task', {
|
||||
taskList: state => state.taskList,
|
||||
selectedGidList: state => state.selectedGidList,
|
||||
selectedGidListCount: state => state.selectedGidList.length
|
||||
}),
|
||||
...mapState('preference', {
|
||||
noConfirmBeforeDelete: state => state.config.noConfirmBeforeDeleteTask
|
||||
}),
|
||||
subnavs () {
|
||||
return [
|
||||
{
|
||||
key: 'active',
|
||||
@@ -72,7 +95,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
title: function () {
|
||||
title () {
|
||||
const subnav = this.subnavs.find((item) => item.key === this.status)
|
||||
return subnav.title
|
||||
}
|
||||
@@ -86,10 +109,304 @@
|
||||
},
|
||||
changeCurrentList () {
|
||||
this.$store.dispatch('task/changeCurrentList', this.status)
|
||||
},
|
||||
directAddTask (uri, options = {}) {
|
||||
const uris = [uri]
|
||||
const payload = {
|
||||
uris,
|
||||
options: {
|
||||
...options
|
||||
}
|
||||
}
|
||||
this.$store.dispatch('task/addUri', payload)
|
||||
.catch((err) => {
|
||||
this.$msg.error(err.message)
|
||||
})
|
||||
},
|
||||
showAddTaskDialog (uri, options = {}) {
|
||||
const {
|
||||
header,
|
||||
...rest
|
||||
} = options
|
||||
console.log('[Motrix] show add task dialog options: ', options)
|
||||
|
||||
const headers = parseHeader(header)
|
||||
const newOptions = {
|
||||
...rest,
|
||||
...headers
|
||||
}
|
||||
|
||||
this.$store.dispatch('app/updateAddTaskUrl', uri)
|
||||
this.$store.dispatch('app/updateAddTaskOptions', newOptions)
|
||||
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
|
||||
},
|
||||
deleteTaskFiles (task) {
|
||||
try {
|
||||
const result = moveTaskFilesToTrash(task)
|
||||
|
||||
if (!result) {
|
||||
throw new Error('task.remove-task-file-fail')
|
||||
}
|
||||
} catch (err) {
|
||||
this.$msg.error(this.$t(err.message))
|
||||
}
|
||||
},
|
||||
removeTask (task, taskName, isRemoveWithFiles = false) {
|
||||
this.$store.dispatch('task/forcePauseTask', task)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.deleteTaskFiles(task)
|
||||
}
|
||||
|
||||
return this.removeTaskItem(task, taskName)
|
||||
})
|
||||
},
|
||||
removeTaskRecord (task, taskName, isRemoveWithFiles = false) {
|
||||
this.$store.dispatch('task/forcePauseTask', task)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.deleteTaskFiles(task)
|
||||
}
|
||||
|
||||
return this.removeTaskRecordItem(task, taskName)
|
||||
})
|
||||
},
|
||||
async removeTaskItem (task, taskName) {
|
||||
try {
|
||||
await this.$store.dispatch('task/removeTask', task)
|
||||
this.$msg.success(this.$t('task.delete-task-success', {
|
||||
taskName
|
||||
}))
|
||||
} catch ({ code }) {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.delete-task-fail', {
|
||||
taskName
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
async removeTaskRecordItem (task, taskName) {
|
||||
try {
|
||||
await this.$store.dispatch('task/removeTaskRecord', task)
|
||||
this.$msg.success(this.$t('task.remove-record-success', {
|
||||
taskName
|
||||
}))
|
||||
} catch ({ code }) {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.remove-record-fail', {
|
||||
taskName
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
removeTasks (taskList, isRemoveWithFiles = false) {
|
||||
const gids = taskList.map((task) => task.gid)
|
||||
this.$store.dispatch('task/batchForcePauseTask', gids)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.batchDeleteTaskFiles(taskList)
|
||||
}
|
||||
|
||||
this.removeTaskItems(gids)
|
||||
})
|
||||
},
|
||||
batchDeleteTaskFiles (taskList) {
|
||||
const promises = taskList.map((task, index) => delayDeleteTaskFiles(task, index * 200))
|
||||
Promise.allSettled(promises).then(results => {
|
||||
console.log('[Motrix] batch delete task files: ', results)
|
||||
})
|
||||
},
|
||||
removeTaskItems (gids) {
|
||||
this.$store.dispatch('task/batchRemoveTask', gids)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.batch-delete-task-success'))
|
||||
})
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.batch-delete-task-fail'))
|
||||
}
|
||||
})
|
||||
},
|
||||
handlePauseTask (payload) {
|
||||
const { task, taskName } = payload
|
||||
this.$msg.info(this.$t('task.download-pause-message', { taskName }))
|
||||
this.$store.dispatch('task/pauseTask', task)
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.pause-task-fail', { taskName }))
|
||||
}
|
||||
})
|
||||
},
|
||||
handleResumeTask (payload) {
|
||||
const { task, taskName } = payload
|
||||
this.$store.dispatch('task/resumeTask', task)
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.resume-task-fail', {
|
||||
taskName
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
handleStopTaskSeeding (payload) {
|
||||
const { task } = payload
|
||||
this.$store.dispatch('task/stopSeeding', task)
|
||||
this.$msg.info({
|
||||
message: this.$t('task.bt-stopping-seeding-tip'),
|
||||
duration: 8000
|
||||
})
|
||||
},
|
||||
handleRestartTask (payload) {
|
||||
const { task, taskName, showDialog } = payload
|
||||
const { gid } = task
|
||||
const uri = getTaskUri(task)
|
||||
|
||||
this.$store.dispatch('task/getTaskOption', gid)
|
||||
.then((data) => {
|
||||
console.log('[Motrix] get task option:', data)
|
||||
const { dir, header, split } = data
|
||||
const options = {
|
||||
dir,
|
||||
header,
|
||||
split,
|
||||
out: taskName
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
this.showAddTaskDialog(uri, options)
|
||||
} else {
|
||||
this.directAddTask(uri, options)
|
||||
this.$store.dispatch('task/removeTaskRecord', task)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleRevealInFolder (payload) {
|
||||
const { path } = payload
|
||||
showItemInFolder(path, {
|
||||
errorMsg: this.$t('task.file-not-exist')
|
||||
})
|
||||
},
|
||||
handleDeleteTask (payload) {
|
||||
const { task, taskName, deleteWithFiles } = payload
|
||||
const { noConfirmBeforeDelete } = this
|
||||
|
||||
if (noConfirmBeforeDelete) {
|
||||
this.removeTask(task, taskName, deleteWithFiles)
|
||||
return
|
||||
}
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.delete-task'),
|
||||
message: this.$t('task.delete-task-confirm', { taskName }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.delete-task-label'),
|
||||
checkboxChecked: deleteWithFiles
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
this.removeTask(task, taskName, checkboxChecked)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDeleteTaskRecord (payload) {
|
||||
const { task, taskName, deleteWithFiles } = payload
|
||||
const { noConfirmBeforeDelete } = this
|
||||
|
||||
if (noConfirmBeforeDelete) {
|
||||
this.removeTaskRecord(task, taskName, deleteWithFiles)
|
||||
return
|
||||
}
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.remove-record'),
|
||||
message: this.$t('task.remove-record-confirm', { taskName }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.remove-record-label'),
|
||||
checkboxChecked: !!deleteWithFiles
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
this.removeTaskRecord(task, taskName, checkboxChecked)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleBatchDeleteTask (payload) {
|
||||
const { deleteWithFiles } = payload
|
||||
const {
|
||||
noConfirmBeforeDelete,
|
||||
selectedGidList,
|
||||
selectedGidListCount,
|
||||
taskList
|
||||
} = this
|
||||
if (selectedGidListCount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedTaskList = taskList.filter((task) => {
|
||||
return selectedGidList.includes(task.gid)
|
||||
})
|
||||
|
||||
if (noConfirmBeforeDelete) {
|
||||
this.removeTasks(selectedTaskList, deleteWithFiles)
|
||||
return
|
||||
}
|
||||
|
||||
const count = `${selectedGidListCount}`
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.delete-selected-task'),
|
||||
message: this.$t('task.batch-delete-task-confirm', { count }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.delete-task-label'),
|
||||
checkboxChecked: deleteWithFiles
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
this.removeTasks(selectedTaskList, checkboxChecked)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleCopyTaskLink (payload) {
|
||||
const { task } = payload
|
||||
const uri = getTaskUri(task)
|
||||
clipboard.writeText(uri)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.copy-link-success'))
|
||||
})
|
||||
},
|
||||
handleShowTaskInfo (payload) {
|
||||
const { task } = payload
|
||||
this.$store.dispatch('task/showTaskDetail', task)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created () {
|
||||
this.changeCurrentList()
|
||||
},
|
||||
mounted () {
|
||||
commands.on('pause-task', this.handlePauseTask)
|
||||
commands.on('resume-task', this.handleResumeTask)
|
||||
commands.on('stop-task-seeding', this.handleStopTaskSeeding)
|
||||
commands.on('restart-task', this.handleRestartTask)
|
||||
commands.on('reveal-in-folder', this.handleRevealInFolder)
|
||||
commands.on('delete-task', this.handleDeleteTask)
|
||||
commands.on('delete-task-record', this.handleDeleteTaskRecord)
|
||||
commands.on('batch-delete-task', this.handleBatchDeleteTask)
|
||||
commands.on('copy-task-link', this.handleCopyTaskLink)
|
||||
commands.on('show-task-info', this.handleShowTaskInfo)
|
||||
},
|
||||
destroyed () {
|
||||
commands.off('pause-task', this.handlePauseTask)
|
||||
commands.off('resume-task', this.handleResumeTask)
|
||||
commands.off('stop-task-seeding', this.handleStopTaskSeeding)
|
||||
commands.off('restart-task', this.handleRestartTask)
|
||||
commands.off('reveal-in-folder', this.handleRevealInFolder)
|
||||
commands.off('delete-task', this.handleDeleteTask)
|
||||
commands.off('delete-task-record', this.handleDeleteTaskRecord)
|
||||
commands.off('batch-delete-task', this.handleBatchDeleteTask)
|
||||
commands.off('copy-task-link', this.handleCopyTaskLink)
|
||||
commands.off('show-task-info', this.handleShowTaskInfo)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,74 +28,28 @@
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col class="torrent-actions" :span="4">
|
||||
<span
|
||||
@click="handleTrashClick"
|
||||
>
|
||||
<span @click="handleTrashClick">
|
||||
<mo-icon name="trash" width="14" height="14" />
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="torrent-file-list">
|
||||
<el-table
|
||||
stripe
|
||||
ref="torrentTable"
|
||||
height="200"
|
||||
:data="files"
|
||||
tooltip-effect="dark"
|
||||
style="width: 100%"
|
||||
@row-dblclick="handleRowDbClick"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="42">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-name')"
|
||||
show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-extension')"
|
||||
width="80">
|
||||
<template slot-scope="scope">{{ scope.row.extension | removeExtensionDot }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-size')"
|
||||
width="90">
|
||||
<template slot-scope="scope">{{ scope.row.length | bytesToSize }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-row :gutter="12">
|
||||
<el-col class="file-filters" :span="8">
|
||||
<el-button-group>
|
||||
<el-button @click="toggleVideoSelection()">
|
||||
<mo-icon name="video" width="12" height="12" />
|
||||
</el-button>
|
||||
<el-button @click="toggleAudioSelection()">
|
||||
<mo-icon name="audio" width="12" height="12" />
|
||||
</el-button>
|
||||
<el-button @click="toggleImageSelection()">
|
||||
<mo-icon name="image" width="12" height="12" />
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-col>
|
||||
<el-col :span="16" style="text-align: right">
|
||||
{{ $t('task.selected-files-sum', { selectedFilesCount, selectedFilesTotalSize }) }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
<mo-task-files
|
||||
ref="torrentFileList"
|
||||
mode="ADD"
|
||||
:files="files"
|
||||
:height="200"
|
||||
@selection-change="handleSelectionChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { isEmpty } from 'lodash'
|
||||
import parseTorrent from 'parse-torrent'
|
||||
import TaskFiles from '@/components/TaskDetail/TaskFiles'
|
||||
import '@/components/Icons/inbox'
|
||||
import '@/components/Icons/video'
|
||||
import '@/components/Icons/audio'
|
||||
import '@/components/Icons/image'
|
||||
import {
|
||||
EMPTY_STRING,
|
||||
NONE_SELECTED_FILES,
|
||||
SELECTED_ALL_FILES
|
||||
} from '@shared/constants'
|
||||
@@ -103,9 +57,6 @@
|
||||
buildFileList,
|
||||
listTorrentFiles,
|
||||
bytesToSize,
|
||||
filterVideoFiles,
|
||||
filterAudioFiles,
|
||||
filterImageFiles,
|
||||
getAsBase64,
|
||||
removeExtensionDot
|
||||
} from '@shared/utils'
|
||||
@@ -113,6 +64,7 @@
|
||||
export default {
|
||||
name: 'mo-select-torrent',
|
||||
components: {
|
||||
[TaskFiles.name]: TaskFiles
|
||||
},
|
||||
filters: {
|
||||
bytesToSize,
|
||||
@@ -122,42 +74,21 @@
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
currentTorrent: '',
|
||||
name: EMPTY_STRING,
|
||||
currentTorrent: EMPTY_STRING,
|
||||
files: [],
|
||||
selectedFiles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('preference', {
|
||||
config: state => state.config
|
||||
}),
|
||||
...mapState('app', {
|
||||
torrents: state => state.addTaskTorrents
|
||||
}),
|
||||
isTorrentsEmpty: function () {
|
||||
...mapState('preference', {
|
||||
config: state => state.config
|
||||
}),
|
||||
isTorrentsEmpty () {
|
||||
return this.torrents.length === 0
|
||||
},
|
||||
selectedFilesCount: function () {
|
||||
return this.selectedFiles.length
|
||||
},
|
||||
selectedFilesTotalSize: function () {
|
||||
const result = this.selectedFiles.reduce((acc, cur) => {
|
||||
return acc + cur.length
|
||||
}, 0)
|
||||
return bytesToSize(result)
|
||||
},
|
||||
selectedFileIndex: function () {
|
||||
const { files, selectedFiles } = this
|
||||
if (files.length === 0 || selectedFiles.length === 0) {
|
||||
return NONE_SELECTED_FILES
|
||||
}
|
||||
if (files.length === selectedFiles.length) {
|
||||
return SELECTED_ALL_FILES
|
||||
}
|
||||
const indexArr = this.selectedFiles.map((item) => item.idx)
|
||||
const result = indexArr.join(',')
|
||||
return result
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -174,9 +105,9 @@
|
||||
|
||||
parseTorrent.remote(file.raw, (err, parsedTorrent) => {
|
||||
if (err) throw err
|
||||
console.log(parsedTorrent)
|
||||
console.log('[Motrix] parsed torrent: ', parsedTorrent)
|
||||
this.files = listTorrentFiles(parsedTorrent.files)
|
||||
this.$refs.torrentTable.toggleAllSelection()
|
||||
this.$refs.torrentFileList.toggleAllSelection()
|
||||
|
||||
getAsBase64(file.raw, (torrent) => {
|
||||
this.name = file.name
|
||||
@@ -184,21 +115,17 @@
|
||||
this.$emit('change', torrent, SELECTED_ALL_FILES)
|
||||
})
|
||||
})
|
||||
},
|
||||
selectedFileIndex () {
|
||||
const { currentTorrent, selectedFileIndex } = this
|
||||
this.$emit('change', currentTorrent, selectedFileIndex)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset () {
|
||||
this.name = ''
|
||||
this.currentTorrent = ''
|
||||
this.name = EMPTY_STRING
|
||||
this.currentTorrent = EMPTY_STRING
|
||||
this.files = []
|
||||
if (this.$refs.torrentTable) {
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
if (this.$refs.torrentFileList) {
|
||||
this.$refs.torrentFileList.clearSelection()
|
||||
}
|
||||
this.$emit('change', '', NONE_SELECTED_FILES)
|
||||
this.$emit('change', EMPTY_STRING, NONE_SELECTED_FILES)
|
||||
},
|
||||
handleChange (file, fileList) {
|
||||
this.$store.dispatch('app/addTaskAddTorrents', { fileList })
|
||||
@@ -210,96 +137,57 @@
|
||||
handleTrashClick () {
|
||||
this.$store.dispatch('app/addTaskAddTorrents', { fileList: [] })
|
||||
},
|
||||
toggleSelection (rows) {
|
||||
if (isEmpty(rows)) {
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
} else {
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
rows.forEach(row => {
|
||||
this.$refs.torrentTable.toggleRowSelection(row)
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleVideoSelection () {
|
||||
const filtered = filterVideoFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
toggleAudioSelection () {
|
||||
const filtered = filterAudioFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
toggleImageSelection () {
|
||||
const filtered = filterImageFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
handleRowDbClick (row, column, event) {
|
||||
this.$refs.torrentTable.toggleRowSelection(row)
|
||||
},
|
||||
handleSelectionChange (val) {
|
||||
this.selectedFiles = val
|
||||
const { currentTorrent } = this
|
||||
this.$emit('change', currentTorrent, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.upload-torrent {
|
||||
.upload-torrent {
|
||||
width: 100%;
|
||||
.el-upload, .el-upload-dragger {
|
||||
width: 100%;
|
||||
.el-upload, .el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
.el-upload-dragger {
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
height: auto;
|
||||
}
|
||||
.upload-inbox-icon {
|
||||
}
|
||||
.el-upload-dragger {
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
height: auto;
|
||||
}
|
||||
.upload-inbox-icon {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.torrent-name {
|
||||
margin-top: 4px;
|
||||
font-size: $--font-size-small;
|
||||
color: $--color-text-secondary;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
.selective-torrent {
|
||||
.torrent-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.torrent-info {
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
.torrent-actions {
|
||||
text-align: right;
|
||||
line-height: 16px;
|
||||
&> span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.torrent-name {
|
||||
margin-top: 4px;
|
||||
font-size: $--font-size-small;
|
||||
color: $--color-text-secondary;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
.selective-torrent {
|
||||
.torrent-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.torrent-info {
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
.torrent-actions {
|
||||
text-align: right;
|
||||
line-height: 16px;
|
||||
&> span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 14px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.file-filters {
|
||||
button {
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
.torrent-file-list {
|
||||
border: 1px solid #ebeef5;
|
||||
border-bottom: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 8px;
|
||||
.el-table th {
|
||||
padding: 2px 0;
|
||||
vertical-align: middle;
|
||||
height: 14px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<el-tooltip
|
||||
class="item hidden-md-and-up"
|
||||
effect="dark"
|
||||
:content="$t('task.new-task')"
|
||||
placement="bottom"
|
||||
:content="$t('task.new-task')"
|
||||
>
|
||||
<i class="task-action" @click.stop="onAddClick">
|
||||
<mo-icon name="menu-add" width="14" height="14" />
|
||||
@@ -15,6 +15,7 @@
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
:content="$t('task.delete-selected-tasks')"
|
||||
v-if="currentList !== 'stopped'"
|
||||
>
|
||||
<i
|
||||
class="task-action"
|
||||
@@ -23,17 +24,32 @@
|
||||
<mo-icon name="delete" width="14" height="14" />
|
||||
</i>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" :content="$t('task.refresh-list')" placement="bottom">
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
:content="$t('task.refresh-list')"
|
||||
>
|
||||
<i class="task-action" @click="onRefreshClick">
|
||||
<mo-icon name="refresh" width="14" height="14" :spin="refreshing" />
|
||||
</i>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" :content="$t('task.resume-all-task')" placement="bottom">
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
:content="$t('task.resume-all-task')"
|
||||
>
|
||||
<i class="task-action" @click="onResumeAllClick">
|
||||
<mo-icon name="task-start-line" width="14" height="14" />
|
||||
</i>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" :content="$t('task.pause-all-task')" placement="bottom">
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
:content="$t('task.pause-all-task')"
|
||||
>
|
||||
<i class="task-action" @click="onPauseAllClick">
|
||||
<mo-icon name="task-pause-line" width="14" height="14" />
|
||||
</i>
|
||||
@@ -41,8 +57,8 @@
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
:content="$t('task.purge-record')"
|
||||
placement="bottom"
|
||||
:content="$t('task.purge-record')"
|
||||
v-if="currentList === 'stopped'"
|
||||
>
|
||||
<i class="task-action" @click="onPurgeRecordClick">
|
||||
@@ -54,11 +70,10 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import { bytesToSize, timeFormat } from '@shared/utils'
|
||||
import {
|
||||
moveTaskFilesToTrash
|
||||
} from '@/components/Native/utils'
|
||||
import '@/components/Icons/menu-add'
|
||||
import '@/components/Icons/refresh'
|
||||
import '@/components/Icons/task-start-line'
|
||||
@@ -72,7 +87,7 @@
|
||||
components: {
|
||||
},
|
||||
props: ['task'],
|
||||
data: function () {
|
||||
data () {
|
||||
return {
|
||||
refreshing: false
|
||||
}
|
||||
@@ -80,8 +95,6 @@
|
||||
computed: {
|
||||
...mapState('task', {
|
||||
currentList: state => state.currentList,
|
||||
taskList: state => state.taskList,
|
||||
selectedGidList: state => state.selectedGidList,
|
||||
selectedGidListCount: state => state.selectedGidList.length
|
||||
})
|
||||
},
|
||||
@@ -90,7 +103,7 @@
|
||||
timeFormat
|
||||
},
|
||||
methods: {
|
||||
refreshSpin: function () {
|
||||
refreshSpin () {
|
||||
this.t && clearTimeout(this.t)
|
||||
|
||||
this.refreshing = true
|
||||
@@ -98,79 +111,15 @@
|
||||
this.refreshing = false
|
||||
}, 500)
|
||||
},
|
||||
delayDeleteTaskFiles (task, delay) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const result = moveTaskFilesToTrash(task)
|
||||
resolve(result)
|
||||
} catch (err) {
|
||||
console.log('[Motrix] batch delay delete task files fail', err)
|
||||
resolve(false)
|
||||
}
|
||||
}, delay)
|
||||
})
|
||||
onBatchDeleteClick (event) {
|
||||
const deleteWithFiles = !!event.shiftKey
|
||||
commands.emit('batch-delete-task', { deleteWithFiles })
|
||||
},
|
||||
batchDeleteTaskFiles (taskList) {
|
||||
const promises = taskList.map((task, index) => this.delayDeleteTaskFiles(task, index * 200))
|
||||
Promise.all(promises).then(values => {
|
||||
console.log(values)
|
||||
})
|
||||
},
|
||||
removeTasks (taskList, isRemoveWithFiles) {
|
||||
const gids = taskList.map((task) => task.gid)
|
||||
this.$store.dispatch('task/batchForcePauseTask', gids)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.batchDeleteTaskFiles(taskList)
|
||||
}
|
||||
|
||||
this.removeTaskItems(gids)
|
||||
})
|
||||
},
|
||||
removeTaskItems (gids) {
|
||||
this.$store.dispatch('task/batchRemoveTask', gids)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.batch-delete-task-success'))
|
||||
})
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.batch-delete-task-fail'))
|
||||
}
|
||||
})
|
||||
},
|
||||
onBatchDeleteClick: function (event) {
|
||||
const self = this
|
||||
const { taskList, selectedGidList, selectedGidListCount } = this
|
||||
if (selectedGidListCount === 0) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedTaskList = taskList.filter((task) => {
|
||||
return selectedGidList.includes(task.gid)
|
||||
})
|
||||
const count = `${selectedGidListCount}`
|
||||
const isChecked = !!event.shiftKey
|
||||
this.$electron.remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.delete-selected-task'),
|
||||
message: this.$t('task.batch-delete-task-confirm', { count }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.delete-task-label'),
|
||||
checkboxChecked: isChecked
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
self.removeTasks(selectedTaskList, checkboxChecked)
|
||||
}
|
||||
})
|
||||
},
|
||||
onRefreshClick: function () {
|
||||
onRefreshClick () {
|
||||
this.refreshSpin()
|
||||
this.$store.dispatch('task/fetchList')
|
||||
},
|
||||
onResumeAllClick: function () {
|
||||
onResumeAllClick () {
|
||||
this.$store.dispatch('task/resumeAllTask')
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.resume-all-task-success'))
|
||||
@@ -181,7 +130,7 @@
|
||||
}
|
||||
})
|
||||
},
|
||||
onPauseAllClick: function () {
|
||||
onPauseAllClick () {
|
||||
this.$store.dispatch('task/pauseAllTask')
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.pause-all-task-success'))
|
||||
@@ -192,7 +141,7 @@
|
||||
}
|
||||
})
|
||||
},
|
||||
onPurgeRecordClick: function () {
|
||||
onPurgeRecordClick () {
|
||||
this.$store.dispatch('task/purgeTaskRecord')
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.purge-record-success'))
|
||||
@@ -203,7 +152,7 @@
|
||||
}
|
||||
})
|
||||
},
|
||||
onAddClick: function () {
|
||||
onAddClick () {
|
||||
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getTaskFullPath,
|
||||
getTaskName
|
||||
} from '@shared/utils'
|
||||
import { getTaskName } from '@shared/utils'
|
||||
import { TASK_STATUS } from '@shared/constants'
|
||||
import { openItem } from '@/components/Native/utils'
|
||||
import { openItem, getTaskFullPath } from '@/utils/native'
|
||||
import TaskItemActions from './TaskItemActions'
|
||||
import TaskProgress from './TaskProgress'
|
||||
import TaskProgressInfo from './TaskProgressInfo'
|
||||
@@ -61,13 +58,14 @@
|
||||
this.toggleTask()
|
||||
}
|
||||
},
|
||||
openTask () {
|
||||
async openTask () {
|
||||
const { taskName } = this
|
||||
this.$msg.info(this.$t('task.opening-task-message', { taskName }))
|
||||
const fullPath = getTaskFullPath(this.task)
|
||||
openItem(fullPath, {
|
||||
errorMsg: this.$t('task.file-not-exist')
|
||||
})
|
||||
const result = await openItem(fullPath)
|
||||
if (result) {
|
||||
this.$msg.error(this.$t('task.file-not-exist'))
|
||||
}
|
||||
},
|
||||
toggleTask () {
|
||||
this.$store.dispatch('task/toggleTask', this.task)
|
||||
@@ -100,8 +98,8 @@
|
||||
}
|
||||
.task-name {
|
||||
color: #505753;
|
||||
margin-bottom: 32px;
|
||||
margin-right: 240px;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-right: 220px;
|
||||
word-break: break-all;
|
||||
min-height: 26px;
|
||||
&> span {
|
||||
|
||||
@@ -36,20 +36,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import is from 'electron-is'
|
||||
import * as clipboard from 'clipboard-polyfill'
|
||||
import { ADD_TASK_TYPE, TASK_STATUS } from '@shared/constants'
|
||||
import {
|
||||
showItemInFolder,
|
||||
moveTaskFilesToTrash
|
||||
} from '@/components/Native/utils'
|
||||
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
import { TASK_STATUS } from '@shared/constants'
|
||||
import {
|
||||
checkTaskIsSeeder,
|
||||
getTaskFullPath,
|
||||
getTaskName,
|
||||
getTaskUri,
|
||||
parseHeader
|
||||
getTaskName
|
||||
} from '@shared/utils'
|
||||
import { getTaskFullPath } from '@/utils/native'
|
||||
import '@/components/Icons/task-start-line'
|
||||
import '@/components/Icons/task-pause-line'
|
||||
import '@/components/Icons/task-stop-line'
|
||||
@@ -76,7 +72,10 @@
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'LIST'
|
||||
default: 'LIST',
|
||||
validator: function (value) {
|
||||
return ['LIST', 'DETAIL'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
task: {
|
||||
type: Object,
|
||||
@@ -84,6 +83,9 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('preference', {
|
||||
noConfirmBeforeDelete: state => state.config.noConfirmBeforeDeleteTask
|
||||
}),
|
||||
taskName () {
|
||||
return getTaskName(this.task)
|
||||
},
|
||||
@@ -102,10 +104,17 @@
|
||||
}
|
||||
},
|
||||
taskCommonActions () {
|
||||
let result = is.renderer() ? ['FOLDER'] : []
|
||||
result = (this.mode === 'LIST')
|
||||
? [...result, 'LINK', 'INFO']
|
||||
: [...result, 'LINK']
|
||||
const { mode } = this
|
||||
const result = is.renderer() ? ['FOLDER'] : []
|
||||
|
||||
switch (mode) {
|
||||
case 'LIST':
|
||||
result.push('LINK', 'INFO')
|
||||
break
|
||||
case 'DETAIL':
|
||||
result.push('LINK')
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
@@ -117,206 +126,67 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteTaskFiles (task) {
|
||||
try {
|
||||
const result = moveTaskFilesToTrash(task)
|
||||
|
||||
if (!result) {
|
||||
throw new Error('task.remove-task-file-fail')
|
||||
}
|
||||
} catch (err) {
|
||||
this.$msg.error(this.$t(err.message))
|
||||
}
|
||||
},
|
||||
removeTask (task, isRemoveWithFiles) {
|
||||
this.$store.dispatch('task/forcePauseTask', task)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.deleteTaskFiles(task)
|
||||
}
|
||||
|
||||
return this.removeTaskItem(task)
|
||||
})
|
||||
},
|
||||
removeTaskItem (task) {
|
||||
return this.$store.dispatch('task/removeTask', this.task)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.delete-task-success', {
|
||||
taskName: this.taskName
|
||||
}))
|
||||
})
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.delete-task-fail', {
|
||||
taskName: this.taskName
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
removeTaskRecord (task, isRemoveWithFiles) {
|
||||
this.$store.dispatch('task/forcePauseTask', task)
|
||||
.finally(() => {
|
||||
if (isRemoveWithFiles) {
|
||||
this.deleteTaskFiles(task)
|
||||
}
|
||||
|
||||
return this.removeTaskRecordItem(task)
|
||||
})
|
||||
},
|
||||
removeTaskRecordItem (task) {
|
||||
return this.$store.dispatch('task/removeTaskRecord', this.task)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.remove-record-success', {
|
||||
taskName: this.taskName
|
||||
}))
|
||||
})
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.remove-record-fail', {
|
||||
taskName: this.taskName
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
onResumeClick () {
|
||||
this.$store.dispatch('task/resumeTask', this.task)
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.resume-task-fail', {
|
||||
taskName: this.taskName
|
||||
}))
|
||||
}
|
||||
})
|
||||
const { task, taskName } = this
|
||||
commands.emit('resume-task', {
|
||||
task,
|
||||
taskName
|
||||
})
|
||||
},
|
||||
onRestartClick (event) {
|
||||
const { task, taskName } = this
|
||||
const { gid, status } = task
|
||||
const uri = getTaskUri(task)
|
||||
const isNeedShowDialog = status === TASK_STATUS.COMPLETE || !!event.altKey
|
||||
this.$store.dispatch('task/getTaskOption', gid)
|
||||
.then((data) => {
|
||||
console.log('[Motrix] get task option:', data)
|
||||
const { dir, header, maxConnectionPerServer } = data
|
||||
const options = {
|
||||
dir,
|
||||
header,
|
||||
maxConnectionPerServer,
|
||||
out: taskName
|
||||
}
|
||||
|
||||
if (isNeedShowDialog) {
|
||||
this.showAddTaskDialog(uri, options)
|
||||
} else {
|
||||
this.directAddTask(uri, options)
|
||||
this.$store.dispatch('task/removeTaskRecord', task)
|
||||
}
|
||||
})
|
||||
},
|
||||
directAddTask (uri, options = {}) {
|
||||
const uris = [uri]
|
||||
const payload = {
|
||||
uris,
|
||||
options: {
|
||||
...options
|
||||
}
|
||||
}
|
||||
this.$store.dispatch('task/addUri', payload)
|
||||
.catch((err) => {
|
||||
this.$msg.error(err.message)
|
||||
})
|
||||
},
|
||||
showAddTaskDialog (uri, options = {}) {
|
||||
const {
|
||||
header,
|
||||
...rest
|
||||
} = options
|
||||
|
||||
const headers = parseHeader(header)
|
||||
const newOptions = {
|
||||
...rest,
|
||||
...headers
|
||||
}
|
||||
|
||||
this.$store.dispatch('app/updateAddTaskUrl', uri)
|
||||
this.$store.dispatch('app/updateAddTaskOptions', newOptions)
|
||||
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
|
||||
const { status } = task
|
||||
const showDialog = status === TASK_STATUS.COMPLETE || !!event.altKey
|
||||
commands.emit('restart-task', {
|
||||
task,
|
||||
taskName,
|
||||
showDialog
|
||||
})
|
||||
},
|
||||
onPauseClick () {
|
||||
this.pauseTask()
|
||||
const { task, taskName } = this
|
||||
commands.emit('pause-task', {
|
||||
task,
|
||||
taskName
|
||||
})
|
||||
},
|
||||
onStopClick () {
|
||||
this.stopSeeding()
|
||||
},
|
||||
stopSeeding () {
|
||||
if (!this.isSeeder) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('task/stopSeeding', this.task)
|
||||
},
|
||||
pauseTask () {
|
||||
const { taskName } = this
|
||||
this.$msg.info(this.$t('task.download-pause-message', { taskName }))
|
||||
this.$store.dispatch('task/pauseTask', this.task)
|
||||
.catch(({ code }) => {
|
||||
if (code === 1) {
|
||||
this.$msg.error(this.$t('task.pause-task-fail', { taskName }))
|
||||
}
|
||||
})
|
||||
|
||||
const { task } = this
|
||||
commands.emit('stop-task-seeding', { task })
|
||||
},
|
||||
onDeleteClick (event) {
|
||||
const self = this
|
||||
const { task } = this
|
||||
const isChecked = !!event.shiftKey
|
||||
this.$electron.remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.delete-task'),
|
||||
message: this.$t('task.delete-task-confirm', { taskName: this.taskName }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.delete-task-label'),
|
||||
checkboxChecked: isChecked
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
self.removeTask(task, checkboxChecked)
|
||||
}
|
||||
const { task, taskName } = this
|
||||
const deleteWithFiles = !!event.shiftKey
|
||||
commands.emit('delete-task', {
|
||||
task,
|
||||
taskName,
|
||||
deleteWithFiles
|
||||
})
|
||||
},
|
||||
onTrashClick (event) {
|
||||
const self = this
|
||||
const { task } = this
|
||||
const isChecked = !!event.shiftKey
|
||||
this.$electron.remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: this.$t('task.remove-record'),
|
||||
message: this.$t('task.remove-record-confirm', { taskName: this.taskName }),
|
||||
buttons: [this.$t('app.yes'), this.$t('app.no')],
|
||||
cancelId: 1,
|
||||
checkboxLabel: this.$t('task.remove-record-label'),
|
||||
checkboxChecked: isChecked
|
||||
}).then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
self.removeTaskRecord(task, checkboxChecked)
|
||||
}
|
||||
const { task, taskName } = this
|
||||
const deleteWithFiles = !!event.shiftKey
|
||||
commands.emit('delete-task-record', {
|
||||
task,
|
||||
taskName,
|
||||
deleteWithFiles
|
||||
})
|
||||
},
|
||||
onFolderClick () {
|
||||
showItemInFolder(this.path, {
|
||||
errorMsg: this.$t('task.file-not-exist')
|
||||
})
|
||||
const { path } = this
|
||||
commands.emit('reveal-in-folder', { path })
|
||||
},
|
||||
onLinkClick () {
|
||||
this.$store.dispatch('app/fetchEngineOptions')
|
||||
.then((data) => {
|
||||
const { btTracker } = data
|
||||
const uri = getTaskUri(this.task, btTracker)
|
||||
clipboard.writeText(uri)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.copy-link-success'))
|
||||
})
|
||||
})
|
||||
const { task } = this
|
||||
commands.emit('copy-task-link', { task })
|
||||
},
|
||||
onInfoClick () {
|
||||
this.$store.dispatch('task/showTaskItemInfoDialog', this.task)
|
||||
const { task } = this
|
||||
commands.emit('show-task-info', { task })
|
||||
},
|
||||
onMoreClick () {
|
||||
}
|
||||
@@ -325,33 +195,36 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-item-actions {
|
||||
// width: 28px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
border: 1px solid $--task-item-action-border-color;
|
||||
color: $--task-item-action-color;
|
||||
background-color: $--task-item-action-background;
|
||||
border-radius: 14px;
|
||||
transition: $--all-transition;
|
||||
&:hover {
|
||||
border-color: $--task-item-action-hover-border-color;
|
||||
color: $--task-item-action-hover-color;
|
||||
background-color: $--task-item-action-hover-background;
|
||||
width: auto;
|
||||
}
|
||||
&> .task-item-action {
|
||||
.task-item-actions {
|
||||
// width: 28px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
border: 1px solid $--task-item-action-border-color;
|
||||
color: $--task-item-action-color;
|
||||
background-color: $--task-item-action-background;
|
||||
border-radius: 14px;
|
||||
transition: $--all-transition;
|
||||
&:hover {
|
||||
border-color: $--task-item-action-hover-border-color;
|
||||
color: $--task-item-action-hover-color;
|
||||
background-color: $--task-item-action-hover-background;
|
||||
width: auto;
|
||||
}
|
||||
&> .task-item-action {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 0 4px;
|
||||
font-size: 0;
|
||||
cursor: pointer;
|
||||
i {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 0 4px;
|
||||
font-size: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
custom-class="task-info-dialog"
|
||||
width="61.8vw"
|
||||
v-if="task"
|
||||
:title="dialogTitle"
|
||||
:show-close="true"
|
||||
:visible.sync="visible"
|
||||
:before-close="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<div class="task-name" :title="taskFullName">
|
||||
<span>{{ taskFullName }}</span>
|
||||
</div>
|
||||
<mo-task-item-actions mode="ITEM" :task="task" />
|
||||
<div class="task-progress">
|
||||
<mo-task-progress
|
||||
:completed="Number(task.completedLength)"
|
||||
:total="Number(task.totalLength)"
|
||||
:status="task.status"
|
||||
/>
|
||||
<mo-task-progress-info :task="task" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import { getTaskName } from '@shared/utils'
|
||||
import TaskItemActions from './TaskItemActions'
|
||||
import TaskProgress from './TaskProgress'
|
||||
import TaskProgressInfo from './TaskProgressInfo'
|
||||
import '@/components/Icons/task-start-line'
|
||||
import '@/components/Icons/task-pause-line'
|
||||
import '@/components/Icons/delete'
|
||||
import '@/components/Icons/folder'
|
||||
import '@/components/Icons/link'
|
||||
import '@/components/Icons/more'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-item-info',
|
||||
components: {
|
||||
[TaskItemActions.name]: TaskItemActions,
|
||||
[TaskProgress.name]: TaskProgress,
|
||||
[TaskProgressInfo.name]: TaskProgressInfo
|
||||
},
|
||||
props: {
|
||||
task: {
|
||||
type: Object
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
taskFullName: function () {
|
||||
return getTaskName(this.task, {
|
||||
defaultName: this.$t('task.get-task-name'),
|
||||
maxLen: -1
|
||||
})
|
||||
},
|
||||
taskName: function () {
|
||||
return getTaskName(this.task, {
|
||||
defaultName: this.$t('task.get-task-name'),
|
||||
maxLen: 32
|
||||
})
|
||||
},
|
||||
dialogTitle: function () {
|
||||
return this.$t('task.task-info-dialog-title', { title: this.taskName })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose (done) {
|
||||
this.$store.dispatch('task/hideTaskItemInfoDialog')
|
||||
},
|
||||
handleClosed (done) {
|
||||
this.$store.dispatch('task/updateCurrentTaskItem', null)
|
||||
},
|
||||
...mapActions('task', [
|
||||
'toggleTask'
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-info-dialog {
|
||||
min-width: 380px;
|
||||
.el-dialog__header {
|
||||
padding-right: 60px;
|
||||
}
|
||||
.el-dialog__body {
|
||||
position: relative;
|
||||
}
|
||||
.task-name {
|
||||
font-size: 14px;
|
||||
color: #505753;
|
||||
line-height: 26px;
|
||||
margin-bottom: 32px;
|
||||
margin-right: 200px;
|
||||
}
|
||||
}
|
||||
.task-item-info {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.task-item-actions {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 20px;
|
||||
}
|
||||
.task-name {
|
||||
color: #505753;
|
||||
margin-bottom: 32px;
|
||||
margin-right: 240px;
|
||||
word-break: break-all;
|
||||
min-height: 26px;
|
||||
&> span {
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
overflow : hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
.task-progress-info {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
min-height: 14px;
|
||||
color: #9B9B9B;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.task-progress-info-left {
|
||||
min-height: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
.task-progress-info-right {
|
||||
min-height: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
.task-speed-info {
|
||||
& > .task-speed-text {
|
||||
margin-left: 8px;
|
||||
& > i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,44 @@
|
||||
<template>
|
||||
<el-row class="task-progress-info">
|
||||
<el-col :span="8" class="task-progress-info-left">
|
||||
<div v-if="task.totalLength > 0">
|
||||
{{ task.completedLength | bytesToSize }} / {{ task.totalLength | bytesToSize }}
|
||||
<el-col :span="6" class="task-progress-info-left">
|
||||
<div v-if="task.completedLength > 0 || task.totalLength > 0">
|
||||
<span>{{ task.completedLength | bytesToSize }}</span>
|
||||
<span v-if="task.totalLength > 0"> / {{ task.totalLength | bytesToSize }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16" class="task-progress-info-right">
|
||||
<el-col :span="18" class="task-progress-info-right">
|
||||
<div class="task-speed-info" v-if="isActive">
|
||||
<span class="task-speed-text" v-if="isBT">
|
||||
<i><mo-icon name="arrow-up" width="10" height="10" /></i>
|
||||
<i>{{ task.uploadSpeed | bytesToSize }}/s</i>
|
||||
</span>
|
||||
<span class="task-speed-text">
|
||||
<i><mo-icon name="arrow-down" width="10" height="10" /></i>
|
||||
<i>{{ task.downloadSpeed | bytesToSize }}/s</i>
|
||||
</span>
|
||||
<span class="task-speed-text" v-if="remaining > 0">
|
||||
{{
|
||||
remaining | timeFormat({
|
||||
prefix: $t('task.remaining-prefix'),
|
||||
i18n: {
|
||||
'gt1d': $t('app.gt1d'),
|
||||
'hour': $t('app.hour'),
|
||||
'minute': $t('app.minute'),
|
||||
'second': $t('app.second')
|
||||
}
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="task-speed-text">
|
||||
<i><mo-icon name="node" width="10" height="10" /></i>
|
||||
<i>{{ task.connections }}</i>
|
||||
</span>
|
||||
<div class="task-speed-text" v-if="isBT">
|
||||
<i><mo-icon name="arrow-up" width="10" height="14" /></i>
|
||||
<span>{{ task.uploadSpeed | bytesToSize }}/s</span>
|
||||
</div>
|
||||
<div class="task-speed-text">
|
||||
<i><mo-icon name="arrow-down" width="10" height="14" /></i>
|
||||
<span>{{ task.downloadSpeed | bytesToSize }}/s</span>
|
||||
</div>
|
||||
<div class="task-speed-text" v-if="remaining > 0">
|
||||
<span>
|
||||
{{
|
||||
remaining | timeFormat({
|
||||
prefix: $t('task.remaining-prefix'),
|
||||
i18n: {
|
||||
'gt1d': $t('app.gt1d'),
|
||||
'hour': $t('app.hour'),
|
||||
'minute': $t('app.minute'),
|
||||
'second': $t('app.second')
|
||||
}
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="task-speed-text" v-if="isBT">
|
||||
<i><mo-icon name="magnet" width="10" height="14" /></i>
|
||||
<span>{{ task.numSeeders }}</span>
|
||||
</div>
|
||||
<div class="task-speed-text">
|
||||
<i><mo-icon name="node" width="10" height="14" /></i>
|
||||
<span>{{ task.connections }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -49,6 +56,7 @@
|
||||
import '@/components/Icons/arrow-up'
|
||||
import '@/components/Icons/arrow-down'
|
||||
import '@/components/Icons/node'
|
||||
import '@/components/Icons/magnet'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-progress-info',
|
||||
@@ -80,27 +88,47 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-progress-info {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
min-height: 14px;
|
||||
color: #9B9B9B;
|
||||
margin-top: 8px;
|
||||
.task-progress-info {
|
||||
font-size: 0.75rem;
|
||||
line-height: 0.875rem;
|
||||
min-height: 0.875rem;
|
||||
color: #9B9B9B;
|
||||
margin-top: 0.5rem;
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
.task-progress-info-left {
|
||||
min-height: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
.task-progress-info-right {
|
||||
min-height: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
.task-speed-info {
|
||||
& > .task-speed-text {
|
||||
margin-left: 8px;
|
||||
& > i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.task-progress-info-left {
|
||||
min-height: 0.875rem;
|
||||
text-align: left;
|
||||
}
|
||||
.task-progress-info-right {
|
||||
min-height: 0.875rem;
|
||||
text-align: right;
|
||||
}
|
||||
.task-speed-info {
|
||||
font-size: 0;
|
||||
& > .task-speed-text {
|
||||
margin-left: 0.375rem;
|
||||
font-size: 0;
|
||||
line-height: 0.875rem;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
& > i, & > span {
|
||||
height: 0.875rem;
|
||||
line-height: 0.875rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
& > i {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
& > span {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<el-tag :effect="theme" class="tag-task-status" :type="type">
|
||||
{{ status && status.toUpperCase() }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { APP_THEME, TASK_STATUS } from '@shared/constants'
|
||||
import colors from '@shared/colors'
|
||||
|
||||
const statusTypeMap = {
|
||||
[TASK_STATUS.ACTIVE]: 'success',
|
||||
[TASK_STATUS.WAITING]: 'info',
|
||||
[TASK_STATUS.PAUSED]: 'info',
|
||||
[TASK_STATUS.ERROR]: 'danger',
|
||||
[TASK_STATUS.COMPLETE]: 'success',
|
||||
[TASK_STATUS.REMOVED]: 'info',
|
||||
[TASK_STATUS.SEEDING]: 'success'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'mo-task-status',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
default: APP_THEME.DARK,
|
||||
validator: function (value) {
|
||||
return [APP_THEME.LIGHT, APP_THEME.DARK].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: TASK_STATUS.ACTIVE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type () {
|
||||
return statusTypeMap[this.status]
|
||||
},
|
||||
color () {
|
||||
return colors[this.status]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
custom-class="panel task-detail-drawer"
|
||||
size="61.8%"
|
||||
v-if="gid"
|
||||
:title="$t('task.task-detail-title')"
|
||||
:with-header="true"
|
||||
:show-close="true"
|
||||
:visible.sync="visible"
|
||||
:before-close="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-tabs
|
||||
tab-position="top"
|
||||
class="task-detail-tab"
|
||||
value="general"
|
||||
:before-leave="handleTabBeforeLeave"
|
||||
@tab-click="handleTabClick"
|
||||
>
|
||||
<el-tab-pane name="general">
|
||||
<span class="task-detail-tab-label" slot="label"><i class="el-icon-info"></i></span>
|
||||
<mo-task-general :task="task" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="activity" lazy>
|
||||
<span class="task-detail-tab-label" slot="label"><i class="el-icon-s-grid"></i></span>
|
||||
<mo-task-activity ref="taskGraphic" :task="task" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="trackers" lazy v-if="isBT">
|
||||
<span class="task-detail-tab-label" slot="label"><i class="el-icon-discover"></i></span>
|
||||
<mo-task-trackers :task="task" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="peers" lazy v-if="isBT">
|
||||
<span class="task-detail-tab-label" slot="label"><i class="el-icon-s-custom"></i></span>
|
||||
<mo-task-peers :peers="peers" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="files" lazy>
|
||||
<span class="task-detail-tab-label" slot="label"><i class="el-icon-files"></i></span>
|
||||
<mo-task-files
|
||||
ref="detailFileList"
|
||||
mode="DETAIL"
|
||||
:files="fileList"
|
||||
@selection-change="handleSelectionChange"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="task-detail-actions">
|
||||
<mo-task-item-actions mode="DETAIL" :task="task" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import { debounce, merge } from 'lodash'
|
||||
import {
|
||||
calcFormLabelWidth,
|
||||
checkTaskIsBT,
|
||||
checkTaskIsSeeder,
|
||||
getFileName,
|
||||
getFileExtension
|
||||
} from '@shared/utils'
|
||||
import { EMPTY_STRING, TASK_STATUS } from '@shared/constants'
|
||||
import TaskItemActions from '@/components/Task/TaskItemActions'
|
||||
import TaskGeneral from './TaskGeneral'
|
||||
import TaskActivity from './TaskActivity'
|
||||
import TaskTrackers from './TaskTrackers'
|
||||
import TaskPeers from './TaskPeers'
|
||||
import TaskFiles from './TaskFiles'
|
||||
|
||||
const cached = {
|
||||
files: []
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'mo-task-detail',
|
||||
components: {
|
||||
[TaskItemActions.name]: TaskItemActions,
|
||||
[TaskGeneral.name]: TaskGeneral,
|
||||
[TaskActivity.name]: TaskActivity,
|
||||
[TaskTrackers.name]: TaskTrackers,
|
||||
[TaskPeers.name]: TaskPeers,
|
||||
[TaskFiles.name]: TaskFiles
|
||||
},
|
||||
props: {
|
||||
gid: {
|
||||
type: String
|
||||
},
|
||||
task: {
|
||||
type: Object
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
peers: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
return {
|
||||
form: {},
|
||||
formLabelWidth: calcFormLabelWidth(locale),
|
||||
locale,
|
||||
activeTab: 'general',
|
||||
graphicWidth: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer: () => is.renderer(),
|
||||
isBT () {
|
||||
return checkTaskIsBT(this.task)
|
||||
},
|
||||
isSeeder () {
|
||||
return checkTaskIsSeeder(this.task)
|
||||
},
|
||||
taskStatus () {
|
||||
const { task, isSeeder } = this
|
||||
if (isSeeder) {
|
||||
return TASK_STATUS.SEEDING
|
||||
} else {
|
||||
return task.status
|
||||
}
|
||||
},
|
||||
fileList () {
|
||||
const { files } = this
|
||||
const result = files.map((item) => {
|
||||
const name = getFileName(item.path)
|
||||
const extension = getFileExtension(name)
|
||||
return {
|
||||
idx: Number(item.index),
|
||||
selected: item.selected === 'true',
|
||||
path: item.path,
|
||||
name,
|
||||
extension,
|
||||
length: item.length,
|
||||
completedLength: item.completedLength
|
||||
}
|
||||
})
|
||||
merge(cached.files, result)
|
||||
|
||||
return cached.files
|
||||
},
|
||||
selectedFileList () {
|
||||
const { fileList } = this
|
||||
const result = fileList.filter((item) => item.selected)
|
||||
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
console.log('resize===>', this.activeTab, this.$refs.taskGraphic)
|
||||
if (this.activeTab === 'activity' && this.$refs.taskGraphic) {
|
||||
this.$refs.taskGraphic.updateGraphicWidth()
|
||||
}
|
||||
}, 300))
|
||||
},
|
||||
destroyed () {
|
||||
cached.files = []
|
||||
window.removeEventListener('resize')
|
||||
},
|
||||
methods: {
|
||||
handleClose (done) {
|
||||
this.$store.dispatch('task/hideTaskDetail')
|
||||
},
|
||||
handleClosed (done) {
|
||||
this.$store.dispatch('task/updateCurrentTaskGid', EMPTY_STRING)
|
||||
this.$store.dispatch('task/updateCurrentTaskItem', null)
|
||||
},
|
||||
handleTabBeforeLeave (activeName, oldActiveName) {
|
||||
this.activeTab = activeName
|
||||
if (oldActiveName !== 'peers') {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('task/toggleEnabledFetchPeers', false)
|
||||
},
|
||||
handleTabClick (tab) {
|
||||
const { name } = tab
|
||||
|
||||
switch (name) {
|
||||
case 'peers':
|
||||
this.$store.dispatch('task/toggleEnabledFetchPeers', true)
|
||||
break
|
||||
case 'files':
|
||||
setImmediate(() => {
|
||||
this.updateFilesListSelection()
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
updateFilesListSelection () {
|
||||
if (!this.$refs.detailFileList) {
|
||||
return
|
||||
}
|
||||
|
||||
const { selectedFileList } = this
|
||||
this.$refs.detailFileList.toggleSelection(selectedFileList)
|
||||
},
|
||||
handleSelectionChange (val) {
|
||||
console.log('task detail handleSelectionChange==>', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-detail-drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.el-drawer__body {
|
||||
position: relative;
|
||||
}
|
||||
.task-detail-actions {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
bottom: 1rem;
|
||||
z-index: inherit;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0;
|
||||
.task-item-actions {
|
||||
display: inline-block;
|
||||
&> .task-item-action {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.task-detail-drawer-title {
|
||||
&> span, &> ul {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-detail-tab {
|
||||
height: 100%;
|
||||
padding: 0.5rem 1.25rem 3.125rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.task-detail-tab-label {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
.el-tabs__content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.el-tab-pane {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<el-form
|
||||
class="mo-task-activity"
|
||||
ref="form"
|
||||
:model="form"
|
||||
:label-width="formLabelWidth"
|
||||
v-if="task"
|
||||
>
|
||||
<div class="graphic-box" ref="graphicBox">
|
||||
<mo-task-graphic
|
||||
:width="graphicWidth"
|
||||
:bitfield="task.bitfield"
|
||||
v-if="graphicWidth > 0"
|
||||
/>
|
||||
</div>
|
||||
<el-form-item :label="`${$t('task.task-progress-info')}: `">
|
||||
<div class="form-static-value" style="overflow: hidden">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="18">
|
||||
<div class="progress-wrapper">
|
||||
<mo-task-progress
|
||||
:completed="Number(task.completedLength)"
|
||||
:total="Number(task.totalLength)"
|
||||
:status="task.status"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
{{ percent }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-static-value">
|
||||
<span>{{ task.completedLength | bytesToSize }}</span>
|
||||
<span v-if="task.totalLength > 0"> / {{ task.totalLength | bytesToSize }}</span>
|
||||
<span class="task-time-remaining" v-if="isActive && remaining > 0">
|
||||
{{
|
||||
remaining | timeFormat({
|
||||
prefix: $t('task.remaining-prefix'),
|
||||
i18n: {
|
||||
'gt1d': $t('app.gt1d'),
|
||||
'hour': $t('app.hour'),
|
||||
'minute': $t('app.minute'),
|
||||
'second': $t('app.second')
|
||||
}
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-num-seeders')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.numSeeders }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-connections')}: `">
|
||||
<div class="form-static-value">
|
||||
{{ task.connections }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-download-speed')}: `">
|
||||
<div class="form-static-value">
|
||||
<span>{{ task.downloadSpeed | bytesToSize }}/s</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-upload-speed')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
<span>{{ task.uploadSpeed | bytesToSize }}/s</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-upload-length')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
<span>{{ task.uploadLength | bytesToSize }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-ratio')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ ratio }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import {
|
||||
bytesToSize,
|
||||
calcFormLabelWidth,
|
||||
calcProgress,
|
||||
calcRatio,
|
||||
checkTaskIsBT,
|
||||
checkTaskIsSeeder,
|
||||
timeFormat,
|
||||
timeRemaining
|
||||
} from '@shared/utils'
|
||||
import { TASK_STATUS } from '@shared/constants'
|
||||
import TaskGraphic from '@/components/TaskGraphic/Index'
|
||||
import TaskProgress from '@/components/Task/TaskProgress'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-activity',
|
||||
components: {
|
||||
[TaskGraphic.name]: TaskGraphic,
|
||||
[TaskProgress.name]: TaskProgress
|
||||
},
|
||||
props: {
|
||||
gid: {
|
||||
type: String
|
||||
},
|
||||
task: {
|
||||
type: Object
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
peers: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
return {
|
||||
form: {},
|
||||
formLabelWidth: calcFormLabelWidth(locale),
|
||||
locale,
|
||||
graphicWidth: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer: () => is.renderer(),
|
||||
isBT () {
|
||||
return checkTaskIsBT(this.task)
|
||||
},
|
||||
isSeeder () {
|
||||
return checkTaskIsSeeder(this.task)
|
||||
},
|
||||
taskStatus () {
|
||||
const { task, isSeeder } = this
|
||||
if (isSeeder) {
|
||||
return TASK_STATUS.SEEDING
|
||||
} else {
|
||||
return task.status
|
||||
}
|
||||
},
|
||||
isActive () {
|
||||
return this.taskStatus === TASK_STATUS.ACTIVE
|
||||
},
|
||||
percent () {
|
||||
const { totalLength, completedLength } = this.task
|
||||
const percent = calcProgress(totalLength, completedLength)
|
||||
return `${percent}%`
|
||||
},
|
||||
remaining () {
|
||||
const { totalLength, completedLength, downloadSpeed } = this.task
|
||||
return timeRemaining(totalLength, completedLength, downloadSpeed)
|
||||
},
|
||||
ratio () {
|
||||
if (!this.isBT) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const { totalLength, uploadLength } = this.task
|
||||
const ratio = calcRatio(totalLength, uploadLength)
|
||||
return ratio
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
bytesToSize,
|
||||
timeFormat
|
||||
},
|
||||
mounted () {
|
||||
setImmediate(() => {
|
||||
this.updateGraphicWidth()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateGraphicWidth () {
|
||||
if (!this.$refs.graphicBox) {
|
||||
return
|
||||
}
|
||||
console.log('updateGraphicWidth===>', this.$refs.graphicBox)
|
||||
this.graphicWidth = this.calcInnerWidth(this.$refs.graphicBox)
|
||||
},
|
||||
calcInnerWidth (ele) {
|
||||
if (!ele) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const style = getComputedStyle(ele, null)
|
||||
const width = style.getPropertyValue('width')
|
||||
return parseInt(width, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.progress-wrapper {
|
||||
padding: 0.6875rem 0 0 0;
|
||||
}
|
||||
|
||||
.task-time-remaining {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="mo-task-files" v-if="files">
|
||||
<div class="mo-table-wrapper">
|
||||
<el-table
|
||||
stripe
|
||||
ref="torrentTable"
|
||||
:height="height"
|
||||
:data="files"
|
||||
tooltip-effect="dark"
|
||||
style="width: 100%"
|
||||
@row-dblclick="handleRowDbClick"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="42">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-name')"
|
||||
min-width="200"
|
||||
show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-extension')"
|
||||
width="80">
|
||||
<template slot-scope="scope">{{ scope.row.extension | removeExtensionDot }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('task.file-size')"
|
||||
align="right"
|
||||
width="85">
|
||||
<template slot-scope="scope">{{ scope.row.length | bytesToSize }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="mode === 'DETAIL'"
|
||||
:label="$t('task.file-completed-size')"
|
||||
align="right"
|
||||
width="95">
|
||||
<template slot-scope="scope">{{ scope.row.completedLength | bytesToSize }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-row class="file-filters" :gutter="12" v-if="mode === 'ADD'">
|
||||
<el-col class="quick-filters" :span="8">
|
||||
<el-button-group>
|
||||
<el-button @click="toggleVideoSelection()">
|
||||
<mo-icon name="video" width="12" height="12" />
|
||||
</el-button>
|
||||
<el-button @click="toggleAudioSelection()">
|
||||
<mo-icon name="audio" width="12" height="12" />
|
||||
</el-button>
|
||||
<el-button @click="toggleImageSelection()">
|
||||
<mo-icon name="image" width="12" height="12" />
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-col>
|
||||
<el-col :span="16" style="text-align: right">
|
||||
{{ $t('task.selected-files-sum', { selectedFilesCount, selectedFilesTotalSize }) }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmpty } from 'lodash'
|
||||
import '@/components/Icons/video'
|
||||
import '@/components/Icons/audio'
|
||||
import '@/components/Icons/image'
|
||||
import {
|
||||
NONE_SELECTED_FILES,
|
||||
SELECTED_ALL_FILES
|
||||
} from '@shared/constants'
|
||||
import {
|
||||
bytesToSize,
|
||||
filterVideoFiles,
|
||||
filterAudioFiles,
|
||||
filterImageFiles,
|
||||
removeExtensionDot
|
||||
} from '@shared/utils'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-files',
|
||||
filters: {
|
||||
bytesToSize,
|
||||
removeExtensionDot
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'ADD',
|
||||
validator: function (value) {
|
||||
return ['ADD', 'DETAIL'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
height: {
|
||||
type: [Number, String]
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedFiles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedFilesCount () {
|
||||
return this.selectedFiles.length
|
||||
},
|
||||
selectedFilesTotalSize () {
|
||||
const result = this.selectedFiles.reduce((acc, cur) => {
|
||||
return acc + cur.length
|
||||
}, 0)
|
||||
return bytesToSize(result)
|
||||
},
|
||||
selectedFileIndex () {
|
||||
const { files, selectedFiles } = this
|
||||
if (files.length === 0 || selectedFiles.length === 0) {
|
||||
return NONE_SELECTED_FILES
|
||||
}
|
||||
if (files.length === selectedFiles.length) {
|
||||
return SELECTED_ALL_FILES
|
||||
}
|
||||
const indexArr = this.selectedFiles.map((item) => item.idx)
|
||||
const result = indexArr.join(',')
|
||||
return result
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedFileIndex () {
|
||||
const { selectedFileIndex } = this
|
||||
this.$emit('selection-change', selectedFileIndex)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleAllSelection () {
|
||||
if (!this.$refs.torrentTable) {
|
||||
return
|
||||
}
|
||||
this.$refs.torrentTable.toggleAllSelection()
|
||||
},
|
||||
clearSelection () {
|
||||
if (!this.$refs.torrentTable) {
|
||||
return
|
||||
}
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
},
|
||||
toggleSelection (rows) {
|
||||
if (isEmpty(rows)) {
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
} else {
|
||||
this.$refs.torrentTable.clearSelection()
|
||||
rows.forEach(row => {
|
||||
this.$refs.torrentTable.toggleRowSelection(row, true)
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleVideoSelection () {
|
||||
const filtered = filterVideoFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
toggleAudioSelection () {
|
||||
const filtered = filterAudioFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
toggleImageSelection () {
|
||||
const filtered = filterImageFiles(this.files)
|
||||
this.toggleSelection(filtered)
|
||||
},
|
||||
handleRowDbClick (row, column, event) {
|
||||
this.$refs.torrentTable.toggleRowSelection(row)
|
||||
},
|
||||
handleSelectionChange (val) {
|
||||
this.selectedFiles = val
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.file-filters {
|
||||
margin-top: 0.5rem;
|
||||
.quick-filters {
|
||||
button {
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<el-form
|
||||
class="mo-task-general"
|
||||
ref="form"
|
||||
:model="form"
|
||||
:label-width="formLabelWidth"
|
||||
v-if="task"
|
||||
>
|
||||
<el-form-item :label="`${$t('task.task-name')}: `">
|
||||
<div class="form-static-value">
|
||||
{{ taskFullName }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-dir')}: `">
|
||||
<el-input placeholder="" readonly v-model="path">
|
||||
<mo-show-in-folder
|
||||
slot="append"
|
||||
v-if="isRenderer"
|
||||
:path="path"
|
||||
/>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-status')}: `">
|
||||
<div class="form-static-value">
|
||||
<mo-task-status :theme="currentTheme" :status="taskStatus" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-error-info')}: `" v-if="task.errorCode && task.errorCode !== '0'">
|
||||
<div class="form-static-value">
|
||||
{{ task.errorCode }} {{ task.errorMessage }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider v-if="isBT">
|
||||
<i class="el-icon-attract"></i>
|
||||
{{ $t('task.task-bittorrent-info') }}
|
||||
</el-divider>
|
||||
|
||||
<el-form-item :label="`${$t('task.task-info-hash')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.infoHash }}
|
||||
<i class="copy-link" @click="handleCopyClick">
|
||||
<mo-icon
|
||||
name="link"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
</i>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-piece-length')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.pieceLength | bytesToSize }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-num-pieces')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.numPieces }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-bittorrent-creation-date')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.bittorrent.creationDate | localeDateTimeFormat(locale) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('task.task-bittorrent-comment')}: `" v-if="isBT">
|
||||
<div class="form-static-value">
|
||||
{{ task.bittorrent.comment }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import { mapState } from 'vuex'
|
||||
import * as clipboard from 'clipboard-polyfill'
|
||||
import {
|
||||
bytesToSize,
|
||||
calcFormLabelWidth,
|
||||
checkTaskIsBT,
|
||||
checkTaskIsSeeder,
|
||||
getTaskName,
|
||||
getTaskUri,
|
||||
localeDateTimeFormat
|
||||
} from '@shared/utils'
|
||||
import { APP_THEME, TASK_STATUS } from '@shared/constants'
|
||||
import { getTaskFullPath } from '@/utils/native'
|
||||
import ShowInFolder from '@/components/Native/ShowInFolder'
|
||||
import TaskStatus from '@/components/Task/TaskStatus'
|
||||
import '@/components/Icons/folder'
|
||||
import '@/components/Icons/link'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-general',
|
||||
components: {
|
||||
[ShowInFolder.name]: ShowInFolder,
|
||||
[TaskStatus.name]: TaskStatus
|
||||
},
|
||||
props: {
|
||||
task: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
return {
|
||||
form: {},
|
||||
formLabelWidth: calcFormLabelWidth(locale),
|
||||
locale
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', {
|
||||
systemTheme: state => state.systemTheme
|
||||
}),
|
||||
...mapState('preference', {
|
||||
theme: state => state.config.theme
|
||||
}),
|
||||
currentTheme () {
|
||||
if (this.theme === APP_THEME.AUTO) {
|
||||
return this.systemTheme
|
||||
} else {
|
||||
return this.theme
|
||||
}
|
||||
},
|
||||
isRenderer: () => is.renderer(),
|
||||
taskFullName () {
|
||||
return getTaskName(this.task, {
|
||||
defaultName: this.$t('task.get-task-name'),
|
||||
maxLen: -1
|
||||
})
|
||||
},
|
||||
taskName () {
|
||||
return getTaskName(this.task, {
|
||||
defaultName: this.$t('task.get-task-name'),
|
||||
maxLen: 32
|
||||
})
|
||||
},
|
||||
isSeeder () {
|
||||
return checkTaskIsSeeder(this.task)
|
||||
},
|
||||
taskStatus () {
|
||||
const { task, isSeeder } = this
|
||||
if (isSeeder) {
|
||||
return TASK_STATUS.SEEDING
|
||||
} else {
|
||||
return task.status
|
||||
}
|
||||
},
|
||||
path () {
|
||||
return getTaskFullPath(this.task)
|
||||
},
|
||||
isBT () {
|
||||
return checkTaskIsBT(this.task)
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
bytesToSize,
|
||||
localeDateTimeFormat
|
||||
},
|
||||
methods: {
|
||||
handleCopyClick () {
|
||||
const { task } = this
|
||||
const uri = getTaskUri(task)
|
||||
clipboard.writeText(uri)
|
||||
.then(() => {
|
||||
this.$msg.success(this.$t('task.copy-link-success'))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.copy-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="mo-task-peers">
|
||||
<div class="mo-table-wrapper">
|
||||
<el-table
|
||||
stripe
|
||||
ref="peerTable"
|
||||
class="mo-peer-table"
|
||||
:data="peers"
|
||||
>
|
||||
<el-table-column
|
||||
:label="`${$t('task.task-peer-host')}: `"
|
||||
min-width="140">
|
||||
<template slot-scope="scope">
|
||||
{{ `${scope.row.ip}:${scope.row.port}` }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="`${$t('task.task-peer-client')}: `"
|
||||
min-width="125">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.peerId | peerIdParser }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="`%`"
|
||||
align="right"
|
||||
width="55">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.bitfield | bitfieldToPercent }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="`↑`"
|
||||
align="right"
|
||||
width="90">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.uploadSpeed | bytesToSize }}/s
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="`↓`"
|
||||
align="right"
|
||||
width="90">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.downloadSpeed | bytesToSize }}/s
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
bitfieldToPercent,
|
||||
bytesToSize,
|
||||
peerIdParser
|
||||
} from '@shared/utils'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-peers',
|
||||
filters: {
|
||||
bitfieldToPercent,
|
||||
bytesToSize,
|
||||
peerIdParser
|
||||
},
|
||||
props: {
|
||||
peers: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-table.mo-peer-table .cell {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="form"
|
||||
:model="form"
|
||||
:label-width="formLabelWidth"
|
||||
v-if="task"
|
||||
>
|
||||
<div
|
||||
class="tracker-list"
|
||||
v-if="announceList"
|
||||
>
|
||||
<el-input
|
||||
readonly
|
||||
autosize
|
||||
type="textarea"
|
||||
auto-complete="off"
|
||||
v-model="announceList">
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import is from 'electron-is'
|
||||
import {
|
||||
calcFormLabelWidth,
|
||||
checkTaskIsBT,
|
||||
checkTaskIsSeeder
|
||||
} from '@shared/utils'
|
||||
import { convertTrackerDataToLine } from '@shared/utils/tracker'
|
||||
import { EMPTY_STRING } from '@shared/constants'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-trackers',
|
||||
props: {
|
||||
task: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const { locale } = this.$store.state.preference.config
|
||||
return {
|
||||
form: {},
|
||||
formLabelWidth: calcFormLabelWidth(locale),
|
||||
locale
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRenderer: () => is.renderer(),
|
||||
isBT () {
|
||||
return checkTaskIsBT(this.task)
|
||||
},
|
||||
isSeeder () {
|
||||
return checkTaskIsSeeder(this.task)
|
||||
},
|
||||
announceList () {
|
||||
if (!this.isBT) {
|
||||
return EMPTY_STRING
|
||||
}
|
||||
|
||||
const { bittorrent } = this.task
|
||||
const data = bittorrent.announceList.map((i) => i[0])
|
||||
return convertTrackerDataToLine(data)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tracker-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: $--font-size-small;
|
||||
textarea {
|
||||
line-height: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<rect
|
||||
:class="klass"
|
||||
:status="status"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:rx="radius"
|
||||
:ry="radius"
|
||||
:x="x"
|
||||
:y="y"
|
||||
>
|
||||
</rect>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'mo-task-graphic-atom',
|
||||
props: {
|
||||
status: {
|
||||
type: Number
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
x: {
|
||||
type: Number
|
||||
},
|
||||
y: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
klass () {
|
||||
const { status } = this
|
||||
return `graphic-atom graphic-atom-s${status}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.graphic-atom {
|
||||
shape-rendering: geometricPrecision;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
.graphic-atom-s0 {
|
||||
fill: $--graphic-atom-color-0;
|
||||
outline: 1px solid $--graphic-atom-outline-color;
|
||||
}
|
||||
.graphic-atom-s1 {
|
||||
fill: $--graphic-atom-color-1;
|
||||
outline: 1px solid $--graphic-atom-outline-color;
|
||||
}
|
||||
.graphic-atom-s2 {
|
||||
fill: $--graphic-atom-color-2;
|
||||
outline: 1px solid $--graphic-atom-outline-color;
|
||||
}
|
||||
.graphic-atom-s3 {
|
||||
fill: $--graphic-atom-color-3;
|
||||
outline: 1px solid $--graphic-atom-outline-color;
|
||||
}
|
||||
.graphic-atom-s4 {
|
||||
fill: $--graphic-atom-color-4;
|
||||
outline: 1px solid $--graphic-atom-outline-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="svg-task-graphic"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:viewBox="box">
|
||||
<g v-for="(row, index) in atoms" :key="`g-${index}`" >
|
||||
<mo-task-graphic-atom
|
||||
v-for="atom in row"
|
||||
:key="`atom-${atom.id}`"
|
||||
:status="atom.status"
|
||||
:width="atomWidth"
|
||||
:height="atomHeight"
|
||||
:radius="atomRadius"
|
||||
:x="atom.x"
|
||||
:y="atom.y"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Atom from './Atom'
|
||||
|
||||
export default {
|
||||
name: 'mo-task-graphic',
|
||||
components: {
|
||||
[Atom.name]: Atom
|
||||
},
|
||||
props: {
|
||||
bitfield: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 240
|
||||
},
|
||||
atomWidth: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
atomHeight: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
atomGutter: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
atomRadius: {
|
||||
type: Number,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
len () {
|
||||
return this.bitfield.length
|
||||
},
|
||||
atomWG () {
|
||||
return this.atomWidth + this.atomGutter
|
||||
},
|
||||
atomHG () {
|
||||
return this.atomHeight + this.atomGutter
|
||||
},
|
||||
columnCount () {
|
||||
const { width, atomWidth, atomWG } = this
|
||||
const result = parseInt((width - atomWidth) / atomWG, 10) + 1
|
||||
return result
|
||||
},
|
||||
rowCount () {
|
||||
const { len, columnCount } = this
|
||||
const result = parseInt((len / columnCount), 10) + 1
|
||||
return result
|
||||
},
|
||||
offset () {
|
||||
const { width, atomWidth, atomWG, columnCount } = this
|
||||
const totalWidth = atomWG * (columnCount - 1) + atomWidth
|
||||
const result = (width - totalWidth) / 2
|
||||
return parseInt(result, 10)
|
||||
},
|
||||
height () {
|
||||
const { atomHeight, atomHG, rowCount, offset } = this
|
||||
const result = atomHG * (rowCount - 1) + atomHeight + offset * 2
|
||||
return parseInt(result, 10)
|
||||
},
|
||||
box () {
|
||||
return `0 0 ${this.width} ${this.height}`
|
||||
},
|
||||
atoms () {
|
||||
const { len, columnCount } = this
|
||||
const result = []
|
||||
let row = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
row.push(this.buildAtom(i))
|
||||
|
||||
if ((i + 1) % columnCount === 0) {
|
||||
result.push(row)
|
||||
row = []
|
||||
}
|
||||
}
|
||||
result.push(row)
|
||||
|
||||
return result
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildAtom (index) {
|
||||
const { bitfield, offset, atomWG, atomHG, columnCount } = this
|
||||
const hIndex = index + 1
|
||||
let chIndex = index % columnCount
|
||||
let rhIndex = parseInt((index / columnCount), 10)
|
||||
chIndex = chIndex < 0 ? 0 : chIndex
|
||||
rhIndex = rhIndex < 0 ? 0 : rhIndex
|
||||
const result = {
|
||||
id: `${hIndex}`,
|
||||
status: Math.floor(parseInt(bitfield[index], 16) / 4),
|
||||
x: offset + chIndex * atomWG,
|
||||
y: offset + rhIndex * atomHG
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -59,6 +59,7 @@
|
||||
.form-actions {
|
||||
background: $--dk-form-actions-background;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: $--dk-panel-background;
|
||||
}
|
||||
@@ -98,13 +99,46 @@
|
||||
|
||||
.mo-speedometer {
|
||||
background-color: $--dk-speedometer-background;
|
||||
border-color: #5f5f5f;
|
||||
border-color: $--dk-speedometer-border-color;
|
||||
}
|
||||
|
||||
.no-task {
|
||||
color: $--dk-no-task-color;
|
||||
}
|
||||
|
||||
.mo-table-wrapper {
|
||||
border-color: $--dk-table-border-color;
|
||||
}
|
||||
|
||||
.graphic-box {
|
||||
border-color: $--dk-task-detail-box-border;
|
||||
background-color: $--dk-graphic-box-background;
|
||||
}
|
||||
|
||||
.graphic-atom-s0 {
|
||||
fill: $--dk-graphic-atom-color-0;
|
||||
}
|
||||
|
||||
.graphic-atom-s1 {
|
||||
fill: $--dk-graphic-atom-color-1;
|
||||
}
|
||||
|
||||
.graphic-atom-s2 {
|
||||
fill: $--dk-graphic-atom-color-2;
|
||||
}
|
||||
|
||||
.graphic-atom-s3 {
|
||||
fill: $--dk-graphic-atom-color-3;
|
||||
}
|
||||
|
||||
.graphic-atom-s4 {
|
||||
fill: $--dk-graphic-atom-color-4;
|
||||
}
|
||||
|
||||
.form-static-value {
|
||||
color: #e7e7e7;
|
||||
}
|
||||
|
||||
/* Element UI
|
||||
-------------------------- */
|
||||
.el-progress-bar__outer {
|
||||
@@ -390,4 +424,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.el-divider {
|
||||
background-color: #666;
|
||||
}
|
||||
.el-divider__text {
|
||||
background-color: $--dk-panel-background;
|
||||
color: #a7a7a7;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ $--dk-task-item-action-color: #eee !default;
|
||||
$--dk-task-item-action-hover-color: #fff !default;
|
||||
$--dk-no-task-color: #aaa !default;
|
||||
$--dk-add-task-dialog-footer-background: #4a4a4a !default;
|
||||
$--dk-task-detail-box-border: #565656 !default;
|
||||
|
||||
/* Preference
|
||||
-------------------------- */
|
||||
@@ -61,12 +62,21 @@ $--dk-preference-form-text-color: #dfdfdf !default;
|
||||
/* Speedometer
|
||||
-------------------------- */
|
||||
$--dk-speedometer-background: #333 !default;
|
||||
$--dk-speedometer-border-color: #ccc !default;
|
||||
$--dk-speedometer-border-color: #5f5f5f !default;
|
||||
$--dk-speedometer-hover-border-color: #9b9b9b !default;
|
||||
$--dk-speedometer-primary-color: $--color-primary !default;
|
||||
$--dk-speedometer-stopped-color: #9b9b9b !default;
|
||||
$--dk-speedometer-text-color: #9b9b9b !default;
|
||||
|
||||
/* Task Graphic
|
||||
-------------------------- */
|
||||
$--dk-graphic-box-background: #3f3f3f !default;
|
||||
$--dk-graphic-atom-color-0: #161b22 !default;
|
||||
$--dk-graphic-atom-color-1: #0e4429 !default;
|
||||
$--dk-graphic-atom-color-2: #006d32 !default;
|
||||
$--dk-graphic-atom-color-3: #26a641 !default;
|
||||
$--dk-graphic-atom-color-4: #39d353 !default;
|
||||
|
||||
/* Element UI
|
||||
-------------------------- */
|
||||
$--dk-dialog-background: #343434 !default;
|
||||
|
||||
@@ -133,6 +133,10 @@ iframe {
|
||||
}
|
||||
}
|
||||
|
||||
.el-drawer__container {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* App Main
|
||||
-------------------------- */
|
||||
#app,
|
||||
@@ -250,6 +254,31 @@ iframe {
|
||||
background: $--form-actions-background;
|
||||
}
|
||||
|
||||
.mo-table-wrapper {
|
||||
border: 1px solid $--table-border-color;
|
||||
border-bottom: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
.el-table th {
|
||||
padding: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.graphic-box {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border: 1px solid $--task-detail-box-border;
|
||||
border-radius: $--border-radius-base;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.form-static-value {
|
||||
word-break: break-all;
|
||||
color: $--input-font-color;
|
||||
}
|
||||
|
||||
@media only screen and (max-width:567px) {
|
||||
.hidden-xs-only {
|
||||
display:none!important
|
||||
|
||||
@@ -54,6 +54,7 @@ $--task-item-action-color: #9B9B9B !default;
|
||||
$--task-item-action-hover-color: #fff !default;
|
||||
$--no-task-color: #eee !default;
|
||||
$--add-task-dialog-footer-background: #f5f5f5 !default;
|
||||
$--task-detail-box-border: #ebeef5 !default;
|
||||
|
||||
/* Preference
|
||||
-------------------------- */
|
||||
@@ -68,6 +69,17 @@ $--speedometer-primary-color: $--color-primary !default;
|
||||
$--speedometer-stopped-color: #9b9b9b !default;
|
||||
$--speedometer-text-color: #9b9b9b !default;
|
||||
|
||||
/* Task Graphic
|
||||
-------------------------- */
|
||||
$--graphic-box-background: transparent !default;
|
||||
$--graphic-atom-outline-color: rgba(27, 31, 35, 0.06) !default;
|
||||
$--graphic-atom-color-0: #ebedf0 !default;
|
||||
$--graphic-atom-color-1: #9be9a8 !default;
|
||||
$--graphic-atom-color-2: #40c463 !default;
|
||||
$--graphic-atom-color-3: #30a14e !default;
|
||||
$--graphic-atom-color-4: #39d353 !default;
|
||||
|
||||
/* Element UI
|
||||
-------------------------- */
|
||||
$--dialog-background: #fff !default;
|
||||
$--table-border-color: #ebeef5 !default;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:secret="rpcSecret"
|
||||
/>
|
||||
<mo-ipc v-if="isRenderer" />
|
||||
<mo-dynamic-tray v-if="enableTraySpeedometer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,19 +18,22 @@
|
||||
import { mapState } from 'vuex'
|
||||
import { getLangDirection } from '@shared/utils'
|
||||
import { APP_THEME } from '@shared/constants'
|
||||
import TitleBar from '@/components/Native/TitleBar'
|
||||
import DynamicTray from '@/components/Native/DynamicTray'
|
||||
import EngineClient from '@/components/Native/EngineClient'
|
||||
import Ipc from '@/components/Native/Ipc'
|
||||
import TitleBar from '@/components/Native/TitleBar'
|
||||
|
||||
export default {
|
||||
name: 'Motrix',
|
||||
components: {
|
||||
[TitleBar.name]: TitleBar,
|
||||
[DynamicTray.name]: DynamicTray,
|
||||
[EngineClient.name]: EngineClient,
|
||||
[Ipc.name]: Ipc
|
||||
[Ipc.name]: Ipc,
|
||||
[TitleBar.name]: TitleBar
|
||||
},
|
||||
computed: {
|
||||
isRenderer () { return is.renderer() },
|
||||
isMac: () => is.macOS(),
|
||||
isRenderer: () => is.renderer(),
|
||||
...mapState('app', {
|
||||
systemTheme: state => state.systemTheme
|
||||
}),
|
||||
@@ -37,43 +41,48 @@
|
||||
showWindowActions: state => {
|
||||
return (is.windows() || is.linux()) && state.config.hideAppMenu
|
||||
},
|
||||
traySpeedometer: state => state.config.traySpeedometer,
|
||||
rpcSecret: state => state.config.rpcSecret,
|
||||
theme: state => state.config.theme,
|
||||
locale: state => state.config.locale,
|
||||
dir: state => getLangDirection(state.config.locale)
|
||||
}),
|
||||
themeClass: function () {
|
||||
themeClass () {
|
||||
if (this.theme === APP_THEME.AUTO) {
|
||||
return `theme-${this.systemTheme}`
|
||||
} else {
|
||||
return `theme-${this.theme}`
|
||||
}
|
||||
},
|
||||
i18nClass: function () {
|
||||
i18nClass () {
|
||||
return `i18n-${this.locale}`
|
||||
},
|
||||
dirClass: function () {
|
||||
dirClass () {
|
||||
return `dir-${this.dir}`
|
||||
},
|
||||
enableTraySpeedometer () {
|
||||
const { traySpeedometer, isMac, isRenderer } = this
|
||||
return traySpeedometer && isMac && isRenderer
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateRootClassName: function () {
|
||||
updateRootClassName () {
|
||||
const { themeClass = '', i18nClass = '', dirClass = '' } = this
|
||||
const className = `${themeClass} ${i18nClass} ${dirClass}`
|
||||
document.documentElement.className = className
|
||||
}
|
||||
},
|
||||
beforeMount: function () {
|
||||
beforeMount () {
|
||||
this.updateRootClassName()
|
||||
},
|
||||
watch: {
|
||||
themeClass: function (val, oldVal) {
|
||||
themeClass (val, oldVal) {
|
||||
this.updateRootClassName()
|
||||
},
|
||||
i18nClass: function (val, oldVal) {
|
||||
i18nClass (val, oldVal) {
|
||||
this.updateRootClassName()
|
||||
},
|
||||
dirClass: function (val, oldVal) {
|
||||
dirClass (val, oldVal) {
|
||||
this.updateRootClassName()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Message } from 'element-ui'
|
||||
import { base64StringToBlob } from 'blob-util'
|
||||
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { buildFileList } from '@shared/utils'
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import { getLocaleManager } from '@/components/Locale'
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
import {
|
||||
initTaskForm,
|
||||
buildUriPayload,
|
||||
buildTorrentPayload
|
||||
} from '@/utils/task'
|
||||
|
||||
const i18n = getLocaleManager().getI18n()
|
||||
|
||||
const updateSystemTheme = (payload = {}) => {
|
||||
const { theme } = payload
|
||||
store.dispatch('app/updateSystemTheme', theme)
|
||||
}
|
||||
|
||||
const updateTheme = (payload = {}) => {
|
||||
const { theme } = payload
|
||||
store.dispatch('preference/updateThemeConfig', theme)
|
||||
}
|
||||
|
||||
const updateTrayFocused = (payload = {}) => {
|
||||
const { focused } = payload
|
||||
store.dispatch('app/updateTrayFocused', focused)
|
||||
}
|
||||
|
||||
const showAboutPanel = () => {
|
||||
store.dispatch('app/showAboutPanel')
|
||||
}
|
||||
|
||||
const addTask = (payload = {}) => {
|
||||
const {
|
||||
type = ADD_TASK_TYPE.URI,
|
||||
uri,
|
||||
silent,
|
||||
...rest
|
||||
} = payload
|
||||
|
||||
const options = {
|
||||
...rest
|
||||
}
|
||||
|
||||
if (type === ADD_TASK_TYPE.URI && uri) {
|
||||
store.dispatch('app/updateAddTaskUrl', uri)
|
||||
}
|
||||
store.dispatch('app/updateAddTaskOptions', options)
|
||||
|
||||
if (silent) {
|
||||
addTaskSilent(type)
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch('app/showAddTaskDialog', type)
|
||||
}
|
||||
|
||||
const addTaskSilent = (type) => {
|
||||
try {
|
||||
addTaskByType(type)
|
||||
} catch (err) {
|
||||
Message.error(i18n.t(err.message))
|
||||
}
|
||||
}
|
||||
|
||||
const addTaskByType = (type) => {
|
||||
const form = initTaskForm(store.state)
|
||||
|
||||
let payload = null
|
||||
if (type === ADD_TASK_TYPE.URI) {
|
||||
payload = buildUriPayload(form)
|
||||
store.dispatch('task/addUri', payload).catch(err => {
|
||||
Message.error(err.message)
|
||||
})
|
||||
} else if (type === ADD_TASK_TYPE.TORRENT) {
|
||||
payload = buildTorrentPayload(form)
|
||||
store.dispatch('task/addTorrent', payload).catch(err => {
|
||||
Message.error(err.message)
|
||||
})
|
||||
} else if (type === 'metalink') {
|
||||
// @TODO addMetalink
|
||||
} else {
|
||||
console.error('addTask fail', form)
|
||||
}
|
||||
}
|
||||
|
||||
const showAddBtTask = () => {
|
||||
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
|
||||
}
|
||||
|
||||
const showAddBtTaskWithFile = (payload = {}) => {
|
||||
const { name, dataURL = '' } = payload
|
||||
if (!dataURL) {
|
||||
return
|
||||
}
|
||||
|
||||
const blob = base64StringToBlob(dataURL, 'application/x-bittorrent')
|
||||
const file = new File([blob], name, { type: 'application/x-bittorrent' })
|
||||
const fileList = buildFileList(file)
|
||||
|
||||
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
|
||||
setTimeout(() => {
|
||||
store.dispatch('app/addTaskAddTorrents', { fileList })
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const navigateTaskList = (payload = {}) => {
|
||||
const { status = 'active' } = payload
|
||||
|
||||
router.push({ path: `/task/${status}` }).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
const navigatePreferences = () => {
|
||||
router.push({ path: '/preference' }).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
const showUnderDevelopmentMessage = () => {
|
||||
Message.info(i18n.t('app.under-development-message'))
|
||||
}
|
||||
|
||||
const pauseTask = () => {
|
||||
store.dispatch('task/batchPauseSelectedTasks')
|
||||
}
|
||||
|
||||
const resumeTask = () => {
|
||||
store.dispatch('task/batchResumeSelectedTasks')
|
||||
}
|
||||
|
||||
const deleteTask = () => {
|
||||
commands.emit('batch-delete-task', {
|
||||
deleteWithFiles: false
|
||||
})
|
||||
}
|
||||
|
||||
const moveTaskUp = () => {
|
||||
showUnderDevelopmentMessage()
|
||||
}
|
||||
|
||||
const moveTaskDown = () => {
|
||||
showUnderDevelopmentMessage()
|
||||
}
|
||||
|
||||
const pauseAllTask = () => {
|
||||
store.dispatch('task/pauseAllTask')
|
||||
}
|
||||
|
||||
const resumeAllTask = () => {
|
||||
store.dispatch('task/resumeAllTask')
|
||||
}
|
||||
|
||||
const selectAllTask = () => {
|
||||
store.dispatch('task/selectAllTask')
|
||||
}
|
||||
|
||||
const fetchPreference = () => {
|
||||
store.dispatch('preference/fetchPreference')
|
||||
}
|
||||
|
||||
commands.register('application:task-list', navigateTaskList)
|
||||
commands.register('application:preferences', navigatePreferences)
|
||||
commands.register('application:about', showAboutPanel)
|
||||
|
||||
commands.register('application:new-task', addTask)
|
||||
commands.register('application:new-bt-task', showAddBtTask)
|
||||
commands.register('application:new-bt-task-with-file', showAddBtTaskWithFile)
|
||||
commands.register('application:pause-task', pauseTask)
|
||||
commands.register('application:resume-task', resumeTask)
|
||||
commands.register('application:delete-task', deleteTask)
|
||||
commands.register('application:move-task-up', moveTaskUp)
|
||||
commands.register('application:move-task-down', moveTaskDown)
|
||||
commands.register('application:pause-all-task', pauseAllTask)
|
||||
commands.register('application:resume-all-task', resumeAllTask)
|
||||
commands.register('application:select-all-task', selectAllTask)
|
||||
|
||||
commands.register('application:update-preference-config', fetchPreference)
|
||||
commands.register('application:update-system-theme', updateSystemTheme)
|
||||
commands.register('application:update-theme', updateTheme)
|
||||
commands.register('application:update-tray-focused', updateTrayFocused)
|
||||
@@ -1,4 +1,6 @@
|
||||
import is from 'electron-is'
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { getCurrentWindow } from '@electron/remote'
|
||||
import Vue from 'vue'
|
||||
import VueI18Next from '@panter/vue-i18next'
|
||||
import { sync } from 'vuex-router-sync'
|
||||
@@ -12,9 +14,45 @@ import store from '@/store'
|
||||
import { getLocaleManager } from '@/components/Locale'
|
||||
import Icon from '@/components/Icons/Icon'
|
||||
import Msg from '@/components/Msg'
|
||||
import { commands } from '@/components/CommandManager/instance'
|
||||
import TrayWorker from '@/workers/tray.worker'
|
||||
|
||||
import '@/components/Theme/Index.scss'
|
||||
|
||||
const updateTray = is.renderer()
|
||||
? async (payload) => {
|
||||
const { tray } = payload
|
||||
if (!tray) {
|
||||
return
|
||||
}
|
||||
|
||||
const ab = await tray.arrayBuffer()
|
||||
ipcRenderer.send('command', 'application:update-tray', ab)
|
||||
}
|
||||
: () => {}
|
||||
|
||||
function initTrayWorker () {
|
||||
const worker = new TrayWorker()
|
||||
|
||||
worker.addEventListener('message', (event) => {
|
||||
const { type, payload } = event.data
|
||||
|
||||
switch (type) {
|
||||
case 'initialized':
|
||||
case 'log':
|
||||
console.log('[Motrix] Log from Tray Worker: ', payload)
|
||||
break
|
||||
case 'tray:drawed':
|
||||
updateTray(payload)
|
||||
break
|
||||
default:
|
||||
console.warn('[Motrix] Tray Worker unhandled message type:', type, payload)
|
||||
}
|
||||
})
|
||||
|
||||
return worker
|
||||
}
|
||||
|
||||
function init (config) {
|
||||
if (is.renderer()) {
|
||||
Vue.use(require('vue-electron'))
|
||||
@@ -36,17 +74,17 @@ function init (config) {
|
||||
Vue.use(Msg, Message, {
|
||||
showClose: true
|
||||
})
|
||||
Vue.component('mo-icon', Icon)
|
||||
|
||||
const loading = Loading.service({
|
||||
fullscreen: true,
|
||||
background: 'rgba(0, 0, 0, 0.1)'
|
||||
})
|
||||
Vue.component('mo-icon', Icon)
|
||||
|
||||
sync(store, router)
|
||||
|
||||
/* eslint-disable no-new */
|
||||
window.app = new Vue({
|
||||
global.app = new Vue({
|
||||
components: { App },
|
||||
router,
|
||||
store,
|
||||
@@ -54,8 +92,14 @@ function init (config) {
|
||||
template: '<App/>'
|
||||
}).$mount('#app')
|
||||
|
||||
global.app.commands = commands
|
||||
require('./commands')
|
||||
|
||||
global.app.trayWorker = initTrayWorker()
|
||||
|
||||
setTimeout(() => {
|
||||
loading.close()
|
||||
getCurrentWindow().setHasShadow(true)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ADD_TASK_TYPE } from '@shared/constants'
|
||||
import api from '@/api'
|
||||
import { getSystemTheme } from '@/components/Native/utils'
|
||||
import { getSystemTheme, isBigSur } from '@/utils/native'
|
||||
|
||||
const BASE_INTERVAL = 1000
|
||||
const PER_INTERVAL = 100
|
||||
@@ -9,6 +9,8 @@ const MAX_INTERVAL = 6000
|
||||
|
||||
const state = {
|
||||
systemTheme: getSystemTheme(),
|
||||
bigSur: isBigSur(),
|
||||
trayFocused: false,
|
||||
aboutPanelVisible: false,
|
||||
engineInfo: {
|
||||
version: '',
|
||||
@@ -34,10 +36,13 @@ const getters = {
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
CHANGE_SYSTEM_THEME (state, theme) {
|
||||
UPDATE_SYSTEM_THEME (state, theme) {
|
||||
state.systemTheme = theme
|
||||
},
|
||||
CHANGE_ABOUT_PANEL_VISIBLE (state, visible) {
|
||||
UPDATE_TRAY_FOCUSED (state, focused) {
|
||||
state.trayFocused = focused
|
||||
},
|
||||
UPDATE_ABOUT_PANEL_VISIBLE (state, visible) {
|
||||
state.aboutPanelVisible = visible
|
||||
},
|
||||
UPDATE_ENGINE_INFO (state, engineInfo) {
|
||||
@@ -49,16 +54,16 @@ const mutations = {
|
||||
UPDATE_GLOBAL_STAT (state, stat) {
|
||||
state.stat = stat
|
||||
},
|
||||
CHANGE_ADD_TASK_VISIBLE (state, visible) {
|
||||
UPDATE_ADD_TASK_VISIBLE (state, visible) {
|
||||
state.addTaskVisible = visible
|
||||
},
|
||||
CHANGE_ADD_TASK_TYPE (state, taskType) {
|
||||
UPDATE_ADD_TASK_TYPE (state, taskType) {
|
||||
state.addTaskType = taskType
|
||||
},
|
||||
CHANGE_ADD_TASK_URL (state, text) {
|
||||
UPDATE_ADD_TASK_URL (state, text) {
|
||||
state.addTaskUrl = text
|
||||
},
|
||||
CHANGE_ADD_TASK_TORRENTS (state, fileList) {
|
||||
UPDATE_ADD_TASK_TORRENTS (state, fileList) {
|
||||
state.addTaskTorrents = [...fileList]
|
||||
},
|
||||
UPDATE_ADD_TASK_OPTIONS (state, options) {
|
||||
@@ -93,13 +98,16 @@ const mutations = {
|
||||
|
||||
const actions = {
|
||||
updateSystemTheme ({ commit }, theme) {
|
||||
commit('CHANGE_SYSTEM_THEME', theme)
|
||||
commit('UPDATE_SYSTEM_THEME', theme)
|
||||
},
|
||||
updateTrayFocused ({ commit }, focused) {
|
||||
commit('UPDATE_TRAY_FOCUSED', focused)
|
||||
},
|
||||
showAboutPanel ({ commit }) {
|
||||
commit('CHANGE_ABOUT_PANEL_VISIBLE', true)
|
||||
commit('UPDATE_ABOUT_PANEL_VISIBLE', true)
|
||||
},
|
||||
hideAboutPanel ({ commit }) {
|
||||
commit('CHANGE_ABOUT_PANEL_VISIBLE', false)
|
||||
commit('UPDATE_ABOUT_PANEL_VISIBLE', false)
|
||||
},
|
||||
fetchEngineInfo ({ commit }) {
|
||||
api.getVersion()
|
||||
@@ -140,22 +148,22 @@ const actions = {
|
||||
commit('INCREASE_INTERVAL', millisecond)
|
||||
},
|
||||
showAddTaskDialog ({ commit }, taskType) {
|
||||
commit('CHANGE_ADD_TASK_TYPE', taskType)
|
||||
commit('CHANGE_ADD_TASK_VISIBLE', true)
|
||||
commit('UPDATE_ADD_TASK_TYPE', taskType)
|
||||
commit('UPDATE_ADD_TASK_VISIBLE', true)
|
||||
},
|
||||
hideAddTaskDialog ({ commit }) {
|
||||
commit('CHANGE_ADD_TASK_VISIBLE', false)
|
||||
commit('CHANGE_ADD_TASK_URL', '')
|
||||
commit('CHANGE_ADD_TASK_TORRENTS', [])
|
||||
commit('UPDATE_ADD_TASK_VISIBLE', false)
|
||||
commit('UPDATE_ADD_TASK_URL', '')
|
||||
commit('UPDATE_ADD_TASK_TORRENTS', [])
|
||||
},
|
||||
changeAddTaskType ({ commit }, taskType) {
|
||||
commit('CHANGE_ADD_TASK_TYPE', taskType)
|
||||
commit('UPDATE_ADD_TASK_TYPE', taskType)
|
||||
},
|
||||
updateAddTaskUrl ({ commit }, text = '') {
|
||||
commit('CHANGE_ADD_TASK_URL', text)
|
||||
updateAddTaskUrl ({ commit }, uri = '') {
|
||||
commit('UPDATE_ADD_TASK_URL', uri)
|
||||
},
|
||||
addTaskAddTorrents ({ commit }, { fileList }) {
|
||||
commit('CHANGE_ADD_TASK_TORRENTS', fileList)
|
||||
commit('UPDATE_ADD_TASK_TORRENTS', fileList)
|
||||
},
|
||||
updateAddTaskOptions ({ commit }, options = {}) {
|
||||
commit('UPDATE_ADD_TASK_OPTIONS', options)
|
||||
|
||||
@@ -14,26 +14,29 @@ const mutations = {
|
||||
}
|
||||
|
||||
const actions = {
|
||||
fetchPreference ({ commit }) {
|
||||
fetchPreference ({ dispatch }) {
|
||||
return new Promise((resolve) => {
|
||||
api.fetchPreference()
|
||||
.then((config) => {
|
||||
commit('UPDATE_PREFERENCE_DATA', config)
|
||||
dispatch('updatePreference', config)
|
||||
resolve(config)
|
||||
})
|
||||
})
|
||||
},
|
||||
save ({ commit, dispatch }, config) {
|
||||
save ({ dispatch }, config) {
|
||||
dispatch('task/saveSession', null, { root: true })
|
||||
if (isEmpty(config)) {
|
||||
return
|
||||
}
|
||||
|
||||
commit('UPDATE_PREFERENCE_DATA', config)
|
||||
dispatch('updatePreference', config)
|
||||
return api.savePreference(config)
|
||||
},
|
||||
changeThemeConfig ({ commit }, theme) {
|
||||
commit('UPDATE_PREFERENCE_DATA', { theme })
|
||||
updateThemeConfig ({ dispatch }, theme) {
|
||||
dispatch('updatePreference', { theme })
|
||||
},
|
||||
updatePreference ({ commit }, config) {
|
||||
commit('UPDATE_PREFERENCE_DATA', config)
|
||||
},
|
||||
fetchBtTracker (_, trackerSource = []) {
|
||||
return fetchBtTrackerFromSource(trackerSource)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import api from '@/api'
|
||||
import { TASK_STATUS } from '@shared/constants'
|
||||
import { intersection } from '@shared/utils'
|
||||
import { EMPTY_STRING, TASK_STATUS } from '@shared/constants'
|
||||
import { checkTaskIsBT, intersection } from '@shared/utils'
|
||||
|
||||
const state = {
|
||||
currentList: 'active',
|
||||
taskItemInfoVisible: false,
|
||||
taskDetailVisible: false,
|
||||
currentTaskGid: EMPTY_STRING,
|
||||
enabledFetchPeers: false,
|
||||
currentTaskItem: null,
|
||||
currentTaskFiles: [],
|
||||
currentTaskPeers: [],
|
||||
seedingList: [],
|
||||
taskList: [],
|
||||
selectedGidList: []
|
||||
}
|
||||
@@ -14,6 +19,9 @@ const getters = {
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
UPDATE_SEEDING_LIST (state, seedingList) {
|
||||
state.seedingList = seedingList
|
||||
},
|
||||
UPDATE_TASK_LIST (state, taskList) {
|
||||
state.taskList = taskList
|
||||
},
|
||||
@@ -23,11 +31,23 @@ const mutations = {
|
||||
CHANGE_CURRENT_LIST (state, currentList) {
|
||||
state.currentList = currentList
|
||||
},
|
||||
CHANGE_TASK_ITEM_INFO_VISIBLE (state, visible) {
|
||||
state.taskItemInfoVisible = visible
|
||||
CHANGE_TASK_DETAIL_VISIBLE (state, visible) {
|
||||
state.taskDetailVisible = visible
|
||||
},
|
||||
UPDATE_CURRENT_TASK_GID (state, gid) {
|
||||
state.currentTaskGid = gid
|
||||
},
|
||||
UPDATE_ENABLED_FETCH_PEERS (state, enabled) {
|
||||
state.enabledFetchPeers = enabled
|
||||
},
|
||||
UPDATE_CURRENT_TASK_ITEM (state, task) {
|
||||
state.currentTaskItem = task
|
||||
},
|
||||
UPDATE_CURRENT_TASK_FILES (state, files) {
|
||||
state.currentTaskFiles = files
|
||||
},
|
||||
UPDATE_CURRENT_TASK_PEERS (state, peers) {
|
||||
state.currentTaskPeers = peers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,15 +81,36 @@ const actions = {
|
||||
dispatch('updateCurrentTaskItem', data)
|
||||
})
|
||||
},
|
||||
showTaskItemInfoDialog ({ commit, dispatch }, task) {
|
||||
dispatch('updateCurrentTaskItem', task)
|
||||
commit('CHANGE_TASK_ITEM_INFO_VISIBLE', true)
|
||||
fetchItemWithPeers ({ dispatch }, gid) {
|
||||
return api.fetchTaskItemWithPeers({ gid })
|
||||
.then((data) => {
|
||||
console.log('fetchItemWithPeers===>', data)
|
||||
dispatch('updateCurrentTaskItem', data)
|
||||
})
|
||||
},
|
||||
hideTaskItemInfoDialog ({ commit }) {
|
||||
commit('CHANGE_TASK_ITEM_INFO_VISIBLE', false)
|
||||
showTaskDetail ({ commit, dispatch }, task) {
|
||||
dispatch('updateCurrentTaskItem', task)
|
||||
commit('UPDATE_CURRENT_TASK_GID', task.gid)
|
||||
commit('CHANGE_TASK_DETAIL_VISIBLE', true)
|
||||
},
|
||||
hideTaskDetail ({ commit }) {
|
||||
commit('CHANGE_TASK_DETAIL_VISIBLE', false)
|
||||
},
|
||||
toggleEnabledFetchPeers ({ commit }, enabled) {
|
||||
commit('UPDATE_ENABLED_FETCH_PEERS', enabled)
|
||||
},
|
||||
updateCurrentTaskItem ({ commit }, task) {
|
||||
commit('UPDATE_CURRENT_TASK_ITEM', task)
|
||||
if (task) {
|
||||
commit('UPDATE_CURRENT_TASK_FILES', task.files)
|
||||
commit('UPDATE_CURRENT_TASK_PEERS', task.peers)
|
||||
} else {
|
||||
commit('UPDATE_CURRENT_TASK_FILES', [])
|
||||
commit('UPDATE_CURRENT_TASK_PEERS', [])
|
||||
}
|
||||
},
|
||||
updateCurrentTaskGid ({ commit }, gid) {
|
||||
commit('UPDATE_CURRENT_TASK_GID', gid)
|
||||
},
|
||||
addUri ({ dispatch }, data) {
|
||||
const { uris, outs, options } = data
|
||||
@@ -115,25 +156,28 @@ const actions = {
|
||||
dispatch('saveSession')
|
||||
})
|
||||
},
|
||||
forcePauseTask (_, task) {
|
||||
forcePauseTask ({ dispatch }, task) {
|
||||
const { gid, status } = task
|
||||
if (status !== TASK_STATUS.ACTIVE) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
return api.forcePauseTask({ gid })
|
||||
},
|
||||
pauseTask ({ dispatch }, task) {
|
||||
const { gid } = task
|
||||
return api.pauseTask({ gid })
|
||||
.catch(() => {
|
||||
return api.forcePauseTask({ gid })
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch('fetchList')
|
||||
dispatch('saveSession')
|
||||
})
|
||||
},
|
||||
pauseTask ({ dispatch }, task) {
|
||||
const { gid } = task
|
||||
const isBT = checkTaskIsBT(task)
|
||||
const promise = isBT ? api.forcePauseTask({ gid }) : api.pauseTask({ gid })
|
||||
promise.finally(() => {
|
||||
dispatch('fetchList')
|
||||
dispatch('saveSession')
|
||||
})
|
||||
return promise
|
||||
},
|
||||
resumeTask ({ dispatch }, task) {
|
||||
const { gid } = task
|
||||
return api.resumeTask({ gid })
|
||||
@@ -159,8 +203,29 @@ const actions = {
|
||||
dispatch('saveSession')
|
||||
})
|
||||
},
|
||||
stopSeeding ({ dispatch }, task) {
|
||||
const { gid } = task
|
||||
addToSeedingList ({ state, commit }, gid) {
|
||||
const { seedingList } = state
|
||||
if (seedingList.includes(gid)) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = [
|
||||
...seedingList,
|
||||
gid
|
||||
]
|
||||
commit('UPDATE_SEEDING_LIST', list)
|
||||
},
|
||||
removeFromSeedingList ({ state, commit }, gid) {
|
||||
const { seedingList } = state
|
||||
const idx = seedingList.indexOf(gid)
|
||||
if (idx === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = [...seedingList.slice(0, idx), ...seedingList.slice(idx + 1)]
|
||||
commit('UPDATE_SEEDING_LIST', list)
|
||||
},
|
||||
stopSeeding ({ dispatch }, { gid }) {
|
||||
const options = {
|
||||
seedTime: 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import is from 'electron-is'
|
||||
import { shell, nativeTheme } from '@electron/remote'
|
||||
import { access, constants } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
import {
|
||||
getFileNameFromFile,
|
||||
isMagnetTask,
|
||||
getSystemMajorVersion
|
||||
} from '@shared/utils'
|
||||
import { APP_THEME, TASK_STATUS } from '@shared/constants'
|
||||
|
||||
export function showItemInFolder (fullPath, { errorMsg }) {
|
||||
if (!fullPath) {
|
||||
return
|
||||
}
|
||||
|
||||
access(fullPath, constants.F_OK, (err) => {
|
||||
console.log(`[Motrix] ${fullPath} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (err) {
|
||||
Message.error(errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
shell.showItemInFolder(fullPath)
|
||||
})
|
||||
}
|
||||
|
||||
export const openItem = async (fullPath) => {
|
||||
if (!fullPath) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await shell.openPath(fullPath)
|
||||
return result
|
||||
}
|
||||
|
||||
export function getTaskFullPath (task) {
|
||||
const { dir, files, bittorrent } = task
|
||||
let result = resolve(dir)
|
||||
|
||||
// Magnet link task
|
||||
if (isMagnetTask(task)) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (bittorrent && bittorrent.info && bittorrent.info.name) {
|
||||
result = resolve(result, bittorrent.info.name)
|
||||
return result
|
||||
}
|
||||
|
||||
const [file] = files
|
||||
const path = file.path ? resolve(file.path) : ''
|
||||
let fileName = ''
|
||||
|
||||
if (path) {
|
||||
result = path
|
||||
} else {
|
||||
if (files && files.length === 1) {
|
||||
fileName = getFileNameFromFile(file)
|
||||
if (fileName) {
|
||||
result = resolve(result, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function moveTaskFilesToTrash (task) {
|
||||
/**
|
||||
* For magnet link tasks, there is bittorrent, but there is no bittorrent.info.
|
||||
* The path is not a complete path before it becomes a BT task.
|
||||
* In order to avoid accidentally deleting the directory
|
||||
* where the task is located, it directly returns true when deleting.
|
||||
*/
|
||||
if (isMagnetTask(task)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { dir, status } = task
|
||||
const path = getTaskFullPath(task)
|
||||
if (!path || dir === path) {
|
||||
throw new Error('task.file-path-error')
|
||||
}
|
||||
|
||||
let deleteResult1 = true
|
||||
access(path, constants.F_OK, (err) => {
|
||||
console.log(`[Motrix] ${path} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (!err) {
|
||||
// Electron >= 12.x
|
||||
// deleteResult1 = shell.trashItem(path)
|
||||
deleteResult1 = shell.moveItemToTrash(path)
|
||||
}
|
||||
})
|
||||
|
||||
// There is no configuration file for the completed task.
|
||||
if (status === TASK_STATUS.COMPLETE) {
|
||||
return deleteResult1
|
||||
}
|
||||
|
||||
let deleteResult2 = true
|
||||
const extraFilePath = `${path}.aria2`
|
||||
access(extraFilePath, constants.F_OK, (err) => {
|
||||
console.log(`[Motrix] ${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
|
||||
if (!err) {
|
||||
// Electron >= 12.x
|
||||
// deleteResult2 = shell.trashItem(extraFilePath)
|
||||
deleteResult2 = shell.moveItemToTrash(extraFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
return deleteResult1 && deleteResult2
|
||||
}
|
||||
|
||||
export function getSystemTheme () {
|
||||
let result = APP_THEME.LIGHT
|
||||
if (!is.macOS()) {
|
||||
return result
|
||||
}
|
||||
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
|
||||
return result
|
||||
}
|
||||
|
||||
export function isBigSur () {
|
||||
return is.macOS() && getSystemMajorVersion() >= 20
|
||||
}
|
||||
|
||||
export const delayDeleteTaskFiles = (task, delay) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const result = moveTaskFilesToTrash(task)
|
||||
resolve(result)
|
||||
} catch (err) {
|
||||
reject(err.message)
|
||||
}
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import {
|
||||
ADD_TASK_TYPE,
|
||||
NONE_SELECTED_FILES,
|
||||
SELECTED_ALL_FILES
|
||||
} from '@shared/constants'
|
||||
import { splitTaskLinks } from '@shared/utils'
|
||||
import { buildOuts } from '@shared/utils/rename'
|
||||
|
||||
export const initTaskForm = state => {
|
||||
const { addTaskUrl, addTaskOptions } = state.app
|
||||
const {
|
||||
allProxy,
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
maxConnectionPerServer,
|
||||
newTaskShowDownloading,
|
||||
split
|
||||
} = state.preference.config
|
||||
const result = {
|
||||
allProxy,
|
||||
cookie: '',
|
||||
dir,
|
||||
engineMaxConnectionPerServer,
|
||||
maxConnectionPerServer,
|
||||
newTaskShowDownloading,
|
||||
out: '',
|
||||
referer: '',
|
||||
selectFile: NONE_SELECTED_FILES,
|
||||
split,
|
||||
torrent: '',
|
||||
uris: addTaskUrl,
|
||||
userAgent: '',
|
||||
...addTaskOptions
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const buildHeader = (form) => {
|
||||
const { userAgent, referer, cookie } = form
|
||||
const result = []
|
||||
|
||||
if (!isEmpty(userAgent)) {
|
||||
result.push(`User-Agent: ${userAgent}`)
|
||||
}
|
||||
if (!isEmpty(referer)) {
|
||||
result.push(`Referer: ${referer}`)
|
||||
}
|
||||
if (!isEmpty(cookie)) {
|
||||
result.push(`Cookie: ${cookie}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const buildOption = (type, form) => {
|
||||
const {
|
||||
allProxy,
|
||||
dir,
|
||||
out,
|
||||
selectFile,
|
||||
split
|
||||
} = form
|
||||
const result = {}
|
||||
|
||||
if (!isEmpty(allProxy)) {
|
||||
result.allProxy = allProxy
|
||||
}
|
||||
|
||||
if (!isEmpty(dir)) {
|
||||
result.dir = dir
|
||||
}
|
||||
|
||||
if (!isEmpty(out)) {
|
||||
result.out = out
|
||||
}
|
||||
|
||||
if (split > 0) {
|
||||
result.split = split
|
||||
}
|
||||
|
||||
if (type === ADD_TASK_TYPE.TORRENT) {
|
||||
if (
|
||||
selectFile !== SELECTED_ALL_FILES &&
|
||||
selectFile !== NONE_SELECTED_FILES
|
||||
) {
|
||||
result.selectFile = selectFile
|
||||
}
|
||||
}
|
||||
|
||||
const header = buildHeader(form)
|
||||
if (!isEmpty(header)) {
|
||||
result.header = header
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const buildUriPayload = (form) => {
|
||||
let { uris, out } = form
|
||||
if (isEmpty(uris)) {
|
||||
throw new Error('task.new-task-uris-required')
|
||||
}
|
||||
|
||||
uris = splitTaskLinks(uris)
|
||||
const outs = buildOuts(uris, out)
|
||||
|
||||
const options = buildOption(ADD_TASK_TYPE.URI, form)
|
||||
const result = {
|
||||
uris,
|
||||
outs,
|
||||
options
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const buildTorrentPayload = (form) => {
|
||||
const { torrent } = form
|
||||
if (isEmpty(torrent)) {
|
||||
throw new Error('task.new-task-torrent-required')
|
||||
}
|
||||
|
||||
const options = buildOption(ADD_TASK_TYPE.TORRENT, form)
|
||||
const result = {
|
||||
torrent,
|
||||
options
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* eslint no-unused-vars: 'off' */
|
||||
import { TRAY_CANVAS_CONFIG } from '@shared/constants'
|
||||
import { draw } from '@shared/utils/tray'
|
||||
|
||||
let idx = 0
|
||||
let canvas
|
||||
|
||||
const initCanvas = () => {
|
||||
if (canvas) {
|
||||
return canvas
|
||||
}
|
||||
|
||||
const { WIDTH, HEIGHT } = TRAY_CANVAS_CONFIG
|
||||
return new OffscreenCanvas(WIDTH, HEIGHT)
|
||||
}
|
||||
|
||||
const drawTray = async (payload) => {
|
||||
self.postMessage({
|
||||
type: 'log',
|
||||
payload
|
||||
})
|
||||
|
||||
if (!canvas) {
|
||||
canvas = initCanvas()
|
||||
}
|
||||
|
||||
try {
|
||||
const tray = await draw({
|
||||
canvas,
|
||||
...payload
|
||||
})
|
||||
|
||||
self.postMessage({
|
||||
type: 'tray:drawed',
|
||||
payload: {
|
||||
idx,
|
||||
tray
|
||||
}
|
||||
})
|
||||
|
||||
idx += 1
|
||||
} catch (error) {
|
||||
logger(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const logger = (text) => {
|
||||
self.postMessage({
|
||||
type: 'log',
|
||||
payload: text
|
||||
})
|
||||
}
|
||||
|
||||
self.postMessage({
|
||||
type: 'initialized',
|
||||
payload: Date.now()
|
||||
})
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
const { type, payload } = event.data
|
||||
switch (type) {
|
||||
case 'tray:draw':
|
||||
drawTray(payload)
|
||||
break
|
||||
default:
|
||||
logger(JSON.stringify(event.data))
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user