Compare commits

...

149 Commits

Author SHA1 Message Date
Dr_rOot 3aa18e7f72 fix: revert mac target 2021-05-07 19:19:10 +08:00
Dr_rOot e939f9e5dc Merge pull request #925 from agalwood/hotfix/app_hang_20210507 2021-05-07 16:40:04 +08:00
Dr_rOot 53ec0b1dee chore: update deps 2021-05-07 14:11:34 +08:00
Dr_rOot fcfd32a71e refactor: task detail style 2021-05-07 14:06:35 +08:00
Dr_rOot 2ff56a3770 refactor: task item style 2021-05-07 14:06:28 +08:00
Dr_rOot 48768f0658 fix: copyright year 2021-05-07 14:03:32 +08:00
Dr_rOot 3e84230b33 fix: app reset engine not start 2021-05-07 14:03:13 +08:00
Dr_rOot 5490946267 fix: app hang caused by child_process spawn
https://github.com/electron/electron/issues/24329#issuecomment-760699187
2021-05-07 14:02:48 +08:00
Dr_rOot 9461381042 chore: bump version v1.6.8 2021-05-06 15:30:01 +08:00
Dr_rOot 364d7a8f66 chore: lock electron-builder version to 22.10.5 2021-05-06 15:27:39 +08:00
Dr_rOot 7a5a1554ca chore: bump version & electron-builder -> 22.10.5 2021-05-06 14:58:50 +08:00
Dr_rOot 8410be59b2 chore: bump version 1.6.6 2021-05-06 14:19:22 +08:00
Dr_rOot 795db0b926 chore: github action build force release 2021-05-06 14:18:30 +08:00
Dr_rOot 079cab8544 chore: bump version v1.6.5 2021-05-06 13:57:32 +08:00
Dr_rOot 7109747e00 chore: update github actions 2021-05-06 13:56:57 +08:00
Dr_rOot 9b698f5a0e chore: bump version v1.6.3 2021-05-06 12:05:25 +08:00
Dr_rOot 2b35fb9bc2 chore: bump version v1.6.1 2021-05-06 12:02:41 +08:00
Dr_rOot 2b80127ad0 docs: update readme i18n 2021-05-06 12:01:57 +08:00
Dr_rOot d47fa3d705 chore: bump version to 1.6.0 2021-05-06 11:24:16 +08:00
Dr_rOot 555db61ec0 Merge pull request #922 from agalwood/hotfix/theme_style_20210506
refactor: task detail dark theme style
2021-05-06 11:09:57 +08:00
Dr_rOot 7c2fc774ca chore: update deps 2021-05-06 10:48:31 +08:00
Dr_rOot 8eeab3d3fa feat: task graphic dark theme style 2021-05-06 10:36:33 +08:00
Dr_rOot 7a5b16aecc refactor: task status theme 2021-05-06 10:36:02 +08:00
Dr_rOot c5f72414e2 refactor: preference store actions 2021-05-06 10:35:26 +08:00
Dr_rOot 4016f5c02e Merge pull request #921 from agalwood/feature/task_detail_20210503 2021-05-05 23:20:09 +08:00
Dr_rOot af7eb1f359 Merge branch 'master' into feature/task_detail_20210503 2021-05-05 23:08:19 +08:00
Dr_rOot f30ed3c8f7 fix: update task peers table column width 2021-05-05 22:56:44 +08:00
Dr_rOot e9e86fbc83 refactor: task detail tab content scrollable 2021-05-05 22:36:14 +08:00
Dr_rOot d7b985bc3f docs: update app features 2021-05-05 16:54:40 +08:00
Dr_rOot e307240a60 fix: remove useless code 2021-05-05 16:52:28 +08:00
Dr_rOot 8dd1d84485 chore: i18n task detail 2021-05-05 16:51:12 +08:00
Dr_rOot 3c7b0b26e3 chore: i18n bt settings 2021-05-05 16:26:01 +08:00
Dr_rOot 778e092c17 docs: update contributing locale guide 2021-05-05 15:54:32 +08:00
Dr_rOot 6e462166d1 chore: i18n task files 2021-05-05 11:35:30 +08:00
Dr_rOot 812b419379 chore: update task list i18n 2021-05-05 10:39:21 +08:00
Dr_rOot 04dcddd3e1 docs: update translation contributors 2021-05-05 09:45:23 +08:00
أحمد الطبراني 1cfec289b7 fix: Complete translation into Arabic (#919)
* refactor: Complete translation into Arabic

* refactor: Complete translation into Arabic

* Update preferences.js
2021-05-05 09:41:03 +08:00
Dr_rOot 2dcfb8c25e refactor: change some log 2021-05-04 20:38:47 +08:00
Dr_rOot 482b9312b1 chore: i18n task detail 2021-05-04 15:30:16 +08:00
Dr_rOot dbb1889fc9 feat: task detail 2021-05-04 15:06:44 +08:00
Dr_rOot a6de12c875 feat: task activity 2021-05-04 15:04:02 +08:00
Dr_rOot d14ddef8e8 feat: task peers 2021-05-04 15:03:33 +08:00
Dr_rOot c8698e5b80 refactor: task files component 2021-05-04 15:03:10 +08:00
Dr_rOot e8c99caf87 feat: task trackers 2021-05-04 14:59:31 +08:00
Dr_rOot f70595915d feat: task general info 2021-05-04 14:58:39 +08:00
Dr_rOot c5962041cb feat: task bitfield graphic 2021-05-04 14:55:39 +08:00
Dr_rOot 28bbf26334 refactor: select torrent file list 2021-05-03 20:00:07 +08:00
Dr_rOot 3d6931f8d8 refactor: main menu quit 2021-05-03 19:43:14 +08:00
Dr_rOot 46d686bee5 fix: remove file to trash for electron v11.x 2021-05-03 19:41:41 +08:00
Dr_rOot 9ffaf1116f Merge pull request #915 from agalwood/feature/bt_config_202104231109 2021-05-03 19:19:57 +08:00
Dr_rOot 2d4a05f3b0 chore: update deps lock 2021-05-03 19:01:54 +08:00
Dr_rOot e04d3a582e fix: store task checkTaskIsBT 2021-05-03 18:51:12 +08:00
Hadi Alqattan 943830f2ed feat: Add Arabic Localization
* Partial Arabic Language support!

* Partial Arabic Language support!

* translate more words...
2021-05-03 18:35:58 +08:00
Dr_rOot 137927d44a chore: revert electron from 12.x to 11.x 2021-05-03 18:33:29 +08:00
Dr_rOot 819f86632e feat: task progress info show seeder number 2021-05-03 18:18:55 +08:00
Dr_rOot 06f881932c refactor: dev env add engine log 2021-05-03 18:17:41 +08:00
Dr_rOot cb845ea65a refactor: update pause task speed use force pause 2021-05-03 18:15:24 +08:00
Dr_rOot 60e4907be6 fix: stop seeding params gid 2021-04-26 15:00:59 +08:00
Dr_rOot bc38978d6a refactor: open url external 2021-04-23 17:37:16 +08:00
Dr_rOot 90c3bbff13 fix: deps electron remote 2021-04-23 17:36:45 +08:00
Dr_rOot 3116c53734 refactor: electron remote 2021-04-23 15:12:40 +08:00
Dr_rOot 78fb6ba455 refactor: css-minimizer-webpack-plugin
replace css-minimizer-webpack-plugin with css-minimizer-webpack-plugin
2021-04-23 14:51:41 +08:00
Dr_rOot 86304c126d chore: update deps 2021-04-23 11:48:56 +08:00
Dr_rOot 3424424e67 feat: preference bt setting bt-save-metadata 2021-04-23 11:39:34 +08:00
Dr_rOot 7206f2fc3c Merge pull request #892 from agalwood/feature/bt_202103131952
feat: new app icon & bt setting
2021-04-01 23:39:57 +08:00
Dr_rOot bd25e566c8 chore: i18n zh-TW bt keep seeding 2021-03-24 23:26:02 +08:00
Dr_rOot f54bc32e90 feat: bt keep seeding 2021-03-24 23:23:04 +08:00
Dr_rOot f6dde55234 feat: new app icon 2021-03-13 21:14:43 +08:00
Dr_rOot 1303713f5e feat: reset session 2021-03-13 20:08:14 +08:00
Dr_rOot 57a5c0c932 Merge pull request #877 from agalwood/feature/engine_refactor_202102182037
refactor: replace forever with child process
2021-03-13 19:50:43 +08:00
Dr_rOot 22a41c332f Merge branch 'master' into feature/engine_refactor_202102182037
# Conflicts:
#	yarn.lock
2021-03-13 18:37:25 +08:00
Dr_rOot 76e7223e85 Merge pull request #875 from agalwood/dependabot/npm_and_yarn/elliptic-6.5.4
chore(deps): bump elliptic from 6.5.3 to 6.5.4
2021-03-13 18:35:15 +08:00
Dr_rOot 014619ed46 chore: update deps 2021-03-13 18:29:02 +08:00
Dr_rOot aec3a25e3a chore: engine error code text remove # 2021-03-13 11:23:57 +08:00
dependabot[bot] 02f6ffa3cb chore(deps): bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-11 07:16:59 +00:00
Dr_rOot 5d32fc633c feat: add more speed options 2021-03-01 11:12:49 +08:00
Dr_rOot cde2832ae3 chore: aria2 conf http accept gzip 2021-03-01 11:10:02 +08:00
Dr_rOot 8656fa0862 refactor: replace forever with child process 2021-03-01 11:09:14 +08:00
Dr_rOot 0cd1ed1e34 Merge pull request #865 from agalwood/feature/tracker_cdn_202102181833
feat: add tracker cdn source
2021-02-18 20:31:17 +08:00
Dr_rOot d58e485846 refactor: auto sync tracker 2021-02-18 19:03:19 +08:00
Dr_rOot a6aca44672 refactor: preference tracker cdn source select 2021-02-18 18:41:57 +08:00
Dr_rOot cc28e4b19b chore: replace default tracker source config 2021-02-18 18:41:14 +08:00
Dr_rOot 599dec1d6e feat: add tracker cdn urls 2021-02-18 18:35:10 +08:00
Dr_rOot d020359058 Merge pull request #851 from upuppz/master
fix: Function overload error, unable to trigger events bound in EngineClient.vue
2021-01-14 20:23:02 +08:00
upuppz 04d68e293b fix: Function overload error, unable to trigger events bound in EngineClient.vue(函数重载错误, 无法触发在 EngineClient.vue 中绑定的事件) 2021-01-12 18:25:48 +08:00
upuppz 6b93a80888 Merge pull request #4 from agalwood/master
Sync
2021-01-12 18:20:49 +08:00
Dr_rOot c3400d936e Merge pull request #844 from alyn3d/master
feat: Added Romanian translation
2021-01-05 11:12:11 +08:00
alyn3d 871be123d4 feat: Added Romanian translation 2021-01-03 00:29:01 +02:00
Dr_rOot bed2148ba0 Merge pull request #843 from agalwood/hotfix/tray_retina_202012292116
refactor: dynamic tray scale
2020-12-29 21:24:16 +08:00
Dr_rOot 8b97c90e88 refactor: dynamic tray scale 2020-12-29 21:16:36 +08:00
Dr_rOot 832befbafd Merge pull request #842 from agalwood/hotfix/style_202012291454
fix: some style improve
2020-12-29 21:05:31 +08:00
Dr_rOot ddbf27e2f9 refactor: form actions position sticky 2020-12-29 14:55:57 +08:00
Dr_rOot 64b30b6c26 fix: task progress info text font style normal 2020-12-29 14:55:38 +08:00
Dr_rOot 32c8767e65 Merge pull request #840 from agalwood/hotfix/multispinner_202012282315
chore: replace multispinner deps
2020-12-28 23:27:48 +08:00
Dr_rOot 69391c6de0 chore: update deps 2020-12-28 23:21:47 +08:00
Dr_rOot fbec4e7c4d Merge pull request #839 from shatyuka/big_sur_tray
fix: big sur tray color
2020-12-28 23:14:13 +08:00
Shatyuka 7640ba583d fix: big sur tray color 2020-12-28 19:45:53 +08:00
Dr_rOot 924b397727 Merge pull request #838 from agalwood/hotfix/tray_workder_202012271447
fix: tray worker cannot find module 'lodash'
2020-12-27 21:45:06 +08:00
Dr_rOot 565afdde74 chore: update deps & change npm to yarn 2020-12-27 20:07:53 +08:00
Dr_rOot 3dfd4ec4da fix: tray worker cannot find module 'lodash' 2020-12-27 14:47:55 +08:00
Dr_rOot 0a21f590ff Merge pull request #836 from shatyuka/apple_silicon
feat: Support Apple Silicon
2020-12-27 14:45:37 +08:00
カワリミ人形 79bf90bb3c docs: Fix installation instructions in README.md (#834) 2020-12-26 21:09:55 +08:00
Shatyuka f4602b4b68 fix: import JSONRPCError 2020-12-26 13:00:29 +08:00
Shatyuka 93a6f1cfbd fix: Shadow not complete 2020-12-26 12:00:08 +08:00
Shatyuka f0d14afb7e chore: update dependency sass-loader 2020-12-26 11:10:02 +08:00
Shatyuka d9a69ae009 fix: use iframe instead of webview 2020-12-26 09:29:26 +08:00
Shatyuka 0f5398b106 fix: engine failed to quit 2020-12-26 09:03:38 +08:00
Shatyuka c6eb96547a fix: tray menu crash 2020-12-26 07:39:00 +08:00
Shatyuka 31ab487d82 feat: Add native arm support for macos aria2
universal binary
2020-12-26 06:03:36 +08:00
Shatyuka 0b2e271663 feat: Update electron to 11 to support Apple Silicon
Update almost all of the dependencies.
This commit only promises the app would be successfully built and run.
2020-12-26 00:09:44 +08:00
jimman2003 a4a4b2321f fix: updated axios and forever-monitor to get rid of error in dev enviroment (#818) 2020-12-09 20:51:18 +08:00
XIU2 ef372da87b fix: replace TrackersListCollection other.txt with http.txt (#809) 2020-11-25 13:59:00 +08:00
Dr_rOot 10cab1a9a0 docs: readme i18n add Italiano 2020-10-09 21:08:55 +08:00
blackcat-917 a4a651b397 feat: Added Italian language (#794)
* Feat: added italian

* Small fix

* Update index.js to match to the original indent style
2020-10-09 20:56:06 +08:00
Dr_rOot b2770795ad docs: i18n add el (Greek) 2020-08-30 13:22:17 +08:00
Akis S f4c8ff66d5 feat: Added Greek language (#774)
* feat: Added Greek language

* Update index.js
2020-08-30 13:17:49 +08:00
Dr_rOot 1b450c8022 docs: i18n add missing locales es & hu 2020-08-04 21:52:14 +08:00
zalnars e5b8846286 feat: Added Hungarian language (#754)
* Added Hungarian language

* Update all.js

* Update app.js

* Update all.js
2020-07-26 21:06:19 +08:00
Samuel Martineau 8de8591725 fix: correction of the french locale (#742)
* Correction of the French locale

- `transfer-speed-upload`and `transfer-speed-download` were the same
- `transfer-speed-unlimited` was still in English

* fix: correction of the french locale

* fix: correction of the french locale
2020-07-15 20:55:17 +08:00
Dr_rOot 20c30279b0 docs: update readme cn 2020-07-04 21:43:00 +08:00
Stanisław Nieradko 9000be502b feat: Added Polish (#731)
* Added Polish

* Sorted imports in src/shared/locales/app.js
2020-07-04 21:36:34 +08:00
Dr_rOot cfe66cf337 Merge pull request #720 from agalwood/feature/tray_speedometer_202006211216
feat: menu bar tray speedometer #643
2020-06-21 21:30:51 +08:00
Dr_rOot 3557d17bb6 chore: update deps, upgrade electron to 9.x
> That was a bug in Chromium, which got fixed by this [commit](https://chromium.googlesource.com/chromium/src/+/09514b7fbd4fb14ce12a43bc7f4807179612fa94) (available in Chrome v83).

https://stackoverflow.com/questions/61268950/offscreen-converttoblob-very-slow
2020-06-21 21:10:22 +08:00
Dr_rOot 2af891aab8 chore: i18n tray speedometer 2020-06-21 16:23:07 +08:00
Dr_rOot 0d276de39b feat: preference tray speedometer setting 2020-06-21 16:12:35 +08:00
Dr_rOot 8a6beda335 feat: tray speedometer 2020-06-21 16:12:04 +08:00
Dr_rOot 68f1cdc4de chore: update copyright year 2020-06-21 15:30:28 +08:00
Dr_rOot 5290fcfa14 feat: dockmanager add handleSpeedChange 2020-06-21 15:21:06 +08:00
Dr_rOot d865b630a3 refactor: rename enable args 2020-06-21 15:19:50 +08:00
Dr_rOot 9d95d294cd refactor: rename store actions mutations 2020-06-21 13:47:02 +08:00
Dr_rOot ea46d9b3c6 refactor: load config fn use ipc invoke 2020-06-21 12:33:28 +08:00
Dr_rOot ef2c992af9 refactor: remove deprecated fns 2020-06-21 12:30:43 +08:00
Dr_rOot 64ee097c85 Merge pull request #718 from agalwood/feature/stop_seeding_202006201236
fix: stop bt task seeding notification #604
2020-06-20 18:29:48 +08:00
Dr_rOot 31517f93cb chore: i18n stopping seeding tip 2020-06-20 15:51:24 +08:00
Dr_rOot 6d54e557b9 fix: oddly stop seeding notify #604 2020-06-20 15:50:18 +08:00
Dr_rOot 3e61adcea3 Merge pull request #716 from agalwood/feature/url_protocol_202005290826
refactor: redesign motrix scheme
2020-06-20 12:34:52 +08:00
Dr_rOot 84d9ced137 Merge pull request #714 from agalwood/hotfix/hide_run_mode_202006192140
fix: preference show run mode only in macOS
2020-06-19 23:01:36 +08:00
Dr_rOot bc45ed9d5b refactor: add jsconfig 2020-06-19 21:44:53 +08:00
Dr_rOot 3d98104dbf fix: preference show run mode only in macOS 2020-06-19 21:42:00 +08:00
Dr_rOot fb6986ff6e docs: update readme app icon 2020-06-19 20:55:07 +08:00
Dr_rOot b8507570c6 refactor: rename initialForm to initForm 2020-06-12 17:22:38 +08:00
Dr_rOot cf3ef7606c fix: new task uri 2020-06-06 23:31:24 +08:00
Dr_rOot 12a9fa92c1 chore: bump version 2020-06-05 23:54:14 +08:00
Dr_rOot 0fd6617eba docs: update readme i18n 2020-06-05 23:52:34 +08:00
Dr_rOot 7ebbf929d5 fix: i18n no-confirm-before-delete-task missed 2020-06-05 23:49:27 +08:00
Dr_rOot e5241d09d7 refactor: protocol manager init 2020-05-29 17:28:03 +08:00
Dr_rOot c9ea1dece2 refactor: motrix url protocol 2020-05-29 17:02:43 +08:00
Dr_rOot 60d4108ddc refactor: promise all to allSettled 2020-05-29 17:00:47 +08:00
Dr_rOot ffc8de4766 refactor: add task utils 2020-05-29 16:58:35 +08:00
212 changed files with 15592 additions and 19633 deletions
+1 -2
View File
@@ -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')
+5 -12
View File
@@ -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: {
+23 -36
View File
@@ -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']
}
@@ -120,7 +116,7 @@ let rendererConfig = {
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]'
}
@@ -138,7 +134,7 @@ let rendererConfig = {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]'
}
@@ -156,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: {
@@ -210,7 +196,8 @@ let rendererConfig = {
minimizer: [
new TerserPlugin({
extractComments: false,
})
}),
new CssMinimizerPlugin(),
],
},
}
@@ -219,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, '\\\\')}"`
@@ -230,8 +219,6 @@ if (devMode) {
* Adjust rendererConfig for production settings
*/
if (!devMode) {
rendererConfig.devtool = ''
rendererConfig.plugins.push(
new CopyWebpackPlugin({
patterns: [{
+28 -36
View File
@@ -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,17 +183,23 @@ 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({
patterns: [{
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+15 -5
View File
@@ -1,7 +1,7 @@
# 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>
## 一款全能的下载工具
@@ -36,7 +36,7 @@ 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
@@ -94,6 +94,7 @@ Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能
- 🔔 下载完成后通知
- 💻 支持触控栏快捷键 (Mac 专享)
- 🤖 常驻系统托盘,操作更加便捷
- 📟 系统托盘速度仪表显示实时速度 (Mac 专享)
- 🌑 深色模式
- 🗑 移除任务时可同时删除相关文件
- 🌍 国际化,[查看已可选的语言](#-国际化)
@@ -115,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'
@@ -133,13 +135,13 @@ export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
### 开发模式
```bash
npm run dev
yarn run dev
```
### 编译打包
```bash
npm run build
yarn run build
```
完成之后可以在项目的 `release` 目录看到编译打包好的应用文件
@@ -164,15 +166,23 @@ 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) |
+14 -5
View File
@@ -1,7 +1,7 @@
# 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
@@ -36,7 +36,7 @@ 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
@@ -96,6 +96,7 @@ 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).
@@ -117,7 +118,7 @@ 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
@@ -127,13 +128,13 @@ npm install
### 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.
@@ -158,15 +159,23 @@ 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) |
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 62 KiB

+2
View File
@@ -40,6 +40,8 @@ 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 ################
Binary file not shown.
+2
View File
@@ -40,6 +40,8 @@ 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 ################
+2
View File
@@ -40,6 +40,8 @@ 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 ################
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/renderer/*"
],
"@shared/*": [
"./src/shared/*"
]
}
},
"exclude": ["node_modules", "dist"]
}
-18287
View File
File diff suppressed because it is too large Load Diff
+81 -69
View File
@@ -1,6 +1,6 @@
{
"name": "Motrix",
"version": "1.5.14",
"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,83 +193,83 @@
]
},
"dependencies": {
"@babel/runtime": "^7.10.2",
"@panter/vue-i18next": "^0.15.2",
"aria2": "^4.1.0",
"axios": "^0.19.2",
"blob-util": "^2.0.2",
"clipboard-polyfill": "^2.8.6",
"electron-debug": "^3.1.0",
"electron-is": "^3.0.0",
"electron-log": "^4.2.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.1",
"element-ui": "^2.13.2",
"forever-monitor": "3.0.0",
"i18next": "^19.4.5",
"lodash": "^4.17.15",
"@babel/runtime": "^7.14.0",
"@motrix/nat-api": "^0.3.1",
"node-fetch": "^2.6.0",
"@panter/vue-i18next": "^0.15.2",
"axios": "^0.21.1",
"bittorrent-peerid": "^1.3.3",
"blob-util": "^2.0.2",
"clipboard-polyfill": "^3.0.3",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.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.3",
"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.3.2",
"vuex": "^3.4.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.10.2",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-transform-runtime": "^7.10.1",
"@babel/preset-env": "^7.10.2",
"@babel/register": "^7.10.1",
"@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.2",
"chalk": "^4.0.0",
"copy-webpack-plugin": "^6.0.2",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3",
"del": "^5.1.0",
"devtron": "^1.4.0",
"electron": "^8.3.1",
"electron-builder": "^22.7.0",
"electron-builder-notarize": "^1.1.2",
"electron-devtools-installer": "^3.0.0",
"electron-notarize": "^0.3.0",
"electron-osx-sign": "^0.4.16",
"eslint": "^7.1.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.3.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.8",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.3",
"url-loader": "^4.1.0",
"vue-loader": "^15.9.2",
"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.11.0",
"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"
}
}
+2 -2
View File
@@ -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>
<% } %>
+164 -57
View File
@@ -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,16 +31,7 @@ import TouchBarManager from './ui/TouchBarManager'
import TrayManager from './ui/TrayManager'
import DockManager from './ui/DockManager'
import ThemeManager from './ui/ThemeManager'
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 { getSessionPath } from './utils'
export default class Application extends EventEmitter {
constructor () {
@@ -40,7 +41,7 @@ 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)
@@ -78,9 +79,27 @@ export default class Application extends EventEmitter {
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)
@@ -129,6 +148,7 @@ export default class Application extends EventEmitter {
} catch (err) {
logger.warn('[Motrix] shutdown engine fail: ', err.message)
} finally {
// no finally
}
}
@@ -143,7 +163,36 @@ export default class Application extends EventEmitter {
initTrayManager () {
this.trayManager = new TrayManager({
theme: this.configManager.getUserConfig('tray-theme')
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)
})
}
@@ -156,12 +205,12 @@ export default class Application extends EventEmitter {
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
}
@@ -177,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)
}
@@ -192,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) {
@@ -214,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)
}
@@ -222,8 +272,10 @@ 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()
@@ -252,10 +304,18 @@ export default class Application extends EventEmitter {
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: {
@@ -409,8 +469,8 @@ export default class Application extends EventEmitter {
initThemeManager () {
this.themeManager = new ThemeManager()
this.themeManager.on('system-theme-change', (theme) => {
this.trayManager.changeIconTheme(theme)
this.sendCommandToAll('application:update-system-theme', theme)
this.trayManager.handleSystemThemeChange(theme)
this.sendCommandToAll('application:update-system-theme', { theme })
})
}
@@ -423,10 +483,6 @@ export default class Application extends EventEmitter {
}
initProtocolManager () {
if (is.dev() || is.mas()) {
return
}
const protocols = this.configManager.getUserConfig('protocols', {})
this.protocolManager = new ProtocolManager({
protocols
@@ -434,10 +490,6 @@ export default class Application extends EventEmitter {
}
handleProtocol (url) {
if (is.dev() || is.mas()) {
return
}
this.show()
this.protocolManager.handle(url)
@@ -454,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
})
})
}
@@ -471,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()
}
@@ -513,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
@@ -537,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()
})
@@ -557,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()
})
@@ -576,14 +652,14 @@ export default class Application extends EventEmitter {
this.on('application:change-theme', (theme) => {
this.themeManager.updateAppAppearance(theme)
this.sendCommandToAll('application:update-theme', theme)
this.sendCommandToAll('application:update-theme', { theme })
})
this.on('application:change-locale', (locale) => {
this.localeManager.changeLanguageByLocale(locale)
.then(() => {
this.trayManager.setup(locale)
this.menuManager.handleLocaleChange(locale)
this.trayManager.handleLocaleChange(locale)
})
})
@@ -641,29 +717,41 @@ 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)
this.sendCommandToAll('application:update-preference-config', { configName })
}
handleEvents () {
@@ -679,7 +767,7 @@ export default class Application extends EventEmitter {
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 {
@@ -687,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)
})
}
@@ -707,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
})
}
}
+1 -1
View File
@@ -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')
}
}
+10 -6
View File
@@ -15,8 +15,8 @@ import {
EMPTY_STRING,
IP_VERSION,
LOGIN_SETTING_OPTIONS,
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
NGOSANG_TRACKERS_BEST_URL_CDN
} from '@shared/constants'
import { separateConfig } from '@shared/utils'
@@ -51,6 +51,8 @@ export default class ConfigManager {
'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),
@@ -98,6 +100,7 @@ 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,
@@ -113,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': {}
@@ -148,8 +152,8 @@ export default class ConfigManager {
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 -82
View File
@@ -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() ? 0 : 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,31 +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 {
this.instance.removeAllListeners('start')
this.instance.removeAllListeners('error')
this.instance.removeAllListeners('stop')
}
}
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 -1
View File
@@ -1,6 +1,6 @@
'use strict'
import Aria2 from 'aria2'
import { Aria2 } from '@shared/aria2'
import logger from './Logger'
import {
+14 -8
View File
@@ -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) {
@@ -66,12 +71,15 @@ export default class ProtocolManager extends EventEmitter {
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]
@@ -79,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 -1
View File
@@ -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')
+6
View File
@@ -1,8 +1,14 @@
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') {
+5 -5
View File
@@ -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" },
+6 -6
View File
@@ -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" },
+5 -5
View File
@@ -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"
}
]
+5 -5
View File
@@ -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" }
]
+6 -6
View File
@@ -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" },
+34 -16
View File
@@ -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,27 +20,43 @@ export default class DockManager extends EventEmitter {
}
}
show = isMac ? () => {
if (app.dock.isVisible()) {
return
show = isMac
? () => {
if (app.dock.isVisible()) {
return
}
return app.dock.show()
}
: () => {}
return app.dock.show()
} : () => {}
hide = isMac
? () => {
if (!app.dock.isVisible()) {
return
}
hide = isMac ? () => {
if (!app.dock.isVisible()) {
return
app.dock.hide()
}
: () => {}
app.dock.hide()
} : () => {}
setBadge = isMac
? (text) => {
app.dock.setBadge(text)
}
: (text) => {}
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) => {}
openDock = isMac
? (path) => {
app.dock.downloadFinished(path)
}
: (path) => {}
}
+1 -1
View File
@@ -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) {
+235 -49
View File
@@ -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,14 +152,17 @@ export default class TrayManager extends EventEmitter {
this.items = flattenMenuItems(this.menu)
}
setup () {
this.build()
setupMenu () {
this.buildMenu()
this.updateContextMenu()
}
init () {
tray = new Tray(this.normalIcon)
initTray () {
const { icon } = this.getIcons()
tray = new Tray(icon)
// tray.setPressedImage(inverseIcon)
tray.setToolTip('Motrix')
}
@@ -77,53 +171,104 @@ export default class TrayManager extends EventEmitter {
tray.on('click', this.handleTrayClick)
// macOS, Windows
tray.on('double-click', this.handleTrayDbClick)
// 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.on('drop-files', this.handleTrayDropFile)
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
handleTrayDropFiles = (event, files) => {
this.emit('drop-files', files)
}
handleTrayDropText = (event, text) => {
this.emit('drop-text', text)
}
toggleSpeedometer (enabled) {
this.speedometer = enabled
}
async renderTray () {
if (this.speedometer) {
return
}
const { icon } = this.getIcons()
tray.setImage(icon)
// tray.setPressedImage(inverseIcon)
this.updateContextMenu()
}
changeIconTheme (theme = APP_THEME.LIGHT) {
if (!is.macOS()) {
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
}
this.setIcons(theme)
this.updateTray()
tray.setContextMenu(this.menu)
}
updateMenuStates (visibleStates, enabledStates, checkedStates) {
@@ -146,24 +291,65 @@ export default class TrayManager extends EventEmitter {
this.updateMenuStates(null, enabledStates, null)
}
updateContextMenu () {
/**
* Linux requires setContextMenu to be called
* in order for the context menu to populate correctly
*/
if (process.platform !== 'linux') {
handleLocaleChange (locale) {
this.setupMenu()
}
handleSpeedometerEnableChange (enabled) {
this.toggleSpeedometer(enabled)
this.renderTray()
}
handleSystemThemeChange (systemTheme = APP_THEME.LIGHT) {
if (!is.macOS()) {
return
}
tray.setContextMenu(this.menu)
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('double-click', this.handleTrayDbClick)
tray.removeListener('right-click', this.handleTrayRightClick)
tray.removeListener('drop-files', this.handleTrayDropFile)
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()
+9 -3
View File
@@ -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)
+14 -2
View File
@@ -9,11 +9,10 @@ import {
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) {
@@ -25,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')
}
@@ -141,3 +144,12 @@ export const getSystemTheme = () => {
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
}
+8 -4
View File
@@ -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) {
+32 -13
View File
@@ -1,7 +1,7 @@
import { ipcRenderer, remote } from 'electron'
import { ipcRenderer } from 'electron'
import is from 'electron-is'
import { isEmpty, clone } from 'lodash'
import Aria2 from 'aria2'
import { Aria2 } from '@shared/aria2'
import {
separateConfig,
compactUndefined,
@@ -12,8 +12,6 @@ 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
@@ -21,8 +19,8 @@ export default class Api {
this.init()
}
init () {
this.config = this.loadConfig()
async init () {
this.config = await this.loadConfig()
this.client = this.initClient()
this.client.open()
@@ -34,17 +32,14 @@ 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)
@@ -260,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])
+9
View File
@@ -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

+2 -1
View File
@@ -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
}
+1 -1
View File
@@ -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/">
&copy;2019 Motrix
&copy;2021 Motrix
</a>
</el-col>
<el-col :span="18" class="copyright-right">
+12 -15
View File
@@ -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 '@/utils/native'
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)
})
}
}
@@ -30,7 +30,7 @@ export default class CommandManager extends EventEmitter {
}
execute (id, ...args) {
var fn = this.commands[id]
const fn = this.commands[id]
if (fn) {
try {
this.emit('beforeExecuteCommand', id)
+20
View File
@@ -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'
}
}
})
+14 -5
View File
@@ -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>
+47 -31
View File
@@ -7,44 +7,44 @@
import { mapState } from 'vuex'
import api from '@/api'
import {
showItemInFolder,
addToRecentTask
getTaskFullPath,
showItemInFolder
} from '@/utils/native'
import {
bytesToSize,
getTaskName,
getTaskFullPath
} from '@shared/utils'
import { checkTaskIsBT, getTaskName } from '@shared/utils'
export default {
name: 'mo-engine-client',
data () {
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) {
const speed = val > 0 ? `${bytesToSize(val)}/s` : ''
this.$electron.ipcRenderer.send('event', 'download-speed-change', speed)
},
numActive (val) {
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) {
@@ -63,8 +63,12 @@
this.$store.dispatch('task/fetchList')
this.$store.dispatch('app/resetInterval')
this.$store.dispatch('task/saveSession')
console.log('aria2 onDownloadStart', event)
const [{ gid }] = event
const { seedingList } = this
if (seedingList.includes(gid)) {
return
}
this.fetchTaskItem({ gid })
.then((task) => {
const taskName = getTaskName(task)
@@ -73,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)
@@ -83,7 +91,6 @@
})
},
onDownloadStop (event) {
console.log('aria2 onDownloadStop')
const [{ gid }] = event
this.fetchTaskItem({ gid })
.then((task) => {
@@ -100,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,
@@ -111,18 +118,25 @@
})
},
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)
@@ -131,8 +145,6 @@
handleDownloadComplete (task, isBT) {
this.$store.dispatch('task/saveSession')
addToRecentTask(task)
const path = getTaskFullPath(task)
this.showTaskCompleteNotify(task, isBT, path)
this.$electron.ipcRenderer.send('event', 'task-download-complete', task, path)
@@ -207,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 () {
@@ -8,6 +8,7 @@
</template>
<script>
import { dialog } from '@electron/remote'
import '@/components/Icons/folder'
export default {
@@ -17,7 +18,7 @@
methods: {
onFolderClick () {
const self = this
this.$electron.remote.dialog.showOpenDialog({
dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory']
}).then(({ canceled, filePaths }) => {
if (canceled || filePaths.length === 0) {
+2 -1
View File
@@ -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'
@@ -29,7 +30,7 @@
},
computed: {
win () {
return this.$electron.remote.getCurrentWindow()
return getCurrentWindow()
}
},
methods: {
@@ -107,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>
@@ -294,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>
@@ -319,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'
@@ -341,7 +356,7 @@
import '@/components/Icons/sync'
import '@/components/Icons/refresh'
const initialForm = (config) => {
const initForm = (config) => {
const {
allProxy,
allProxyBackup,
@@ -395,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 {
@@ -508,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'),
@@ -524,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
}
+82 -11
View File
@@ -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"
@@ -209,7 +258,6 @@
import ThemeSwitcher from '@/components/Preference/ThemeSwitcher'
import { availableLanguages, getLanguage } from '@shared/locales'
import { getLocaleManager } from '@/components/Locale'
import { prettifyDir } from '@/utils/native'
import {
calcFormLabelWidth,
checkIsNeedRestart,
@@ -217,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,
@@ -234,15 +284,20 @@
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
seedRatio,
seedTime,
taskNotification,
theme
theme,
traySpeedometer
} = config
const result = {
autoHideWindow,
btSaveMetadata,
continue: config.continue,
dir,
engineMaxConnectionPerServer,
hideAppMenu,
keepSeeding,
keepWindowState,
locale,
maxConcurrentDownloads,
@@ -254,8 +309,11 @@
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
seedRatio,
seedTime,
taskNotification,
theme
theme,
traySpeedometer
}
return result
}
@@ -269,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 {
@@ -282,6 +340,7 @@
},
computed: {
isRenderer: () => is.renderer(),
isMac: () => is.macOS(),
isMas: () => is.mas(),
isLinux () { return is.linux() },
title () {
@@ -321,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'
@@ -328,6 +395,10 @@
{
label: '10 MB/s',
value: '10M'
},
{
label: '20 MB/s',
value: '20M'
}
]
},
@@ -353,9 +424,6 @@
showHideAppMenuOption () {
return is.windows() || is.linux()
},
downloadDir () {
return prettifyDir(this.form.dir)
},
...mapState('preference', {
config: state => state.config
})
@@ -369,24 +437,27 @@
},
handleThemeChange (theme) {
this.form.theme = theme
// this.$store.dispatch('preference/changeThemeConfig', theme)
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
}
+2 -5
View File
@@ -56,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;
+12 -125
View File
@@ -172,41 +172,15 @@
import { isEmpty } from 'lodash'
import SelectDirectory from '@/components/Native/SelectDirectory'
import SelectTorrent from '@/components/Task/SelectTorrent'
import { prettifyDir } from '@/utils/native'
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,
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 default {
name: 'mo-add-task',
components: {
@@ -242,9 +216,6 @@
}),
taskType () {
return this.type
},
downloadDir () {
return prettifyDir(this.form.dir)
}
},
watch: {
@@ -279,7 +250,7 @@
}
},
handleOpen () {
this.form = initialForm(this.$store.state)
this.form = initTaskForm(this.$store.state)
if (this.taskType === ADD_TASK_TYPE.URI) {
this.autofillResourceLink()
setTimeout(() => {
@@ -334,101 +305,17 @@
},
reset () {
this.showAdvanced = false
this.form = initialForm(this.$store.state)
},
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,
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 = 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)
})
@@ -456,7 +343,7 @@
})
}
} catch (err) {
this.$msg.error(err.message)
this.$msg.error(this.$t(err.message))
}
})
}
+33 -30
View File
@@ -33,6 +33,7 @@
</template>
<script>
import { dialog } from '@electron/remote'
import { mapState } from 'vuex'
import * as clipboard from 'clipboard-polyfill'
@@ -170,35 +171,33 @@
return this.removeTaskRecordItem(task, taskName)
})
},
removeTaskItem (task, taskName) {
return this.$store.dispatch('task/removeTask', task)
.then(() => {
this.$msg.success(this.$t('task.delete-task-success', {
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
}))
})
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.delete-task-fail', {
taskName
}))
}
})
}
}
},
removeTaskRecordItem (task, taskName) {
return this.$store.dispatch('task/removeTaskRecord', task)
.then(() => {
this.$msg.success(this.$t('task.remove-record-success', {
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
}))
})
.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)
@@ -213,8 +212,8 @@
},
batchDeleteTaskFiles (taskList) {
const promises = taskList.map((task, index) => delayDeleteTaskFiles(task, index * 200))
Promise.all(promises).then(values => {
console.log('[Motrix] batch delete task files: ', values)
Promise.allSettled(promises).then(results => {
console.log('[Motrix] batch delete task files: ', results)
})
},
removeTaskItems (gids) {
@@ -252,6 +251,10 @@
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
@@ -292,7 +295,7 @@
return
}
this.$electron.remote.dialog.showMessageBox({
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-task'),
message: this.$t('task.delete-task-confirm', { taskName }),
@@ -315,7 +318,7 @@
return
}
this.$electron.remote.dialog.showMessageBox({
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.remove-record'),
message: this.$t('task.remove-record-confirm', { taskName }),
@@ -351,7 +354,7 @@
}
const count = `${selectedGidListCount}`
this.$electron.remote.dialog.showMessageBox({
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-selected-task'),
message: this.$t('task.batch-delete-task-confirm', { count }),
@@ -375,7 +378,7 @@
},
handleShowTaskInfo (payload) {
const { task } = payload
this.$store.dispatch('task/showTaskItemInfoDialog', task)
this.$store.dispatch('task/showTaskDetail', task)
}
},
created () {
+61 -173
View File
@@ -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,8 +74,8 @@
},
data () {
return {
name: '',
currentTorrent: '',
name: EMPTY_STRING,
currentTorrent: EMPTY_STRING,
files: [],
selectedFiles: []
}
@@ -137,27 +89,6 @@
}),
isTorrentsEmpty () {
return this.torrents.length === 0
},
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: {
@@ -176,7 +107,7 @@
if (err) throw err
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>
+9 -11
View File
@@ -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 '@/utils/native'
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 {
@@ -43,9 +43,9 @@
import { TASK_STATUS } from '@shared/constants'
import {
checkTaskIsSeeder,
getTaskFullPath,
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'
@@ -72,7 +72,10 @@
props: {
mode: {
type: String,
default: 'LIST'
default: 'LIST',
validator: function (value) {
return ['LIST', 'DETAIL'].indexOf(value) !== -1
}
},
task: {
type: Object,
@@ -101,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
},
@@ -185,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 () {
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
})
},
dialogTitle () {
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,38 +1,44 @@
<template>
<el-row class="task-progress-info">
<el-col :span="8" class="task-progress-info-left">
<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>
<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>
@@ -50,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',
@@ -81,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>
+45 -1
View File
@@ -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;
+12 -3
View File
@@ -9,6 +9,7 @@
:secret="rpcSecret"
/>
<mo-ipc v-if="isRenderer" />
<mo-dynamic-tray v-if="enableTraySpeedometer" />
</div>
</template>
@@ -17,18 +18,21 @@
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: {
isMac: () => is.macOS(),
isRenderer: () => is.renderer(),
...mapState('app', {
systemTheme: state => state.systemTheme
@@ -37,6 +41,7 @@
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,
@@ -54,6 +59,10 @@
},
dirClass () {
return `dir-${this.dir}`
},
enableTraySpeedometer () {
const { traySpeedometer, isMac, isRenderer } = this
return traySpeedometer && isMac && isRenderer
}
},
methods: {
+79 -11
View File
@@ -7,43 +7,110 @@ 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 = (theme) => {
const updateSystemTheme = (payload = {}) => {
const { theme } = payload
store.dispatch('app/updateSystemTheme', theme)
}
const updateTheme = (theme) => {
store.dispatch('preference/changeThemeConfig', 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 showAddTask = (taskType = ADD_TASK_TYPE.URI, uri = '') => {
if (taskType === ADD_TASK_TYPE.URI && uri) {
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/showAddTaskDialog', taskType)
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 = (fileName, base64Data = '') => {
const blob = base64StringToBlob(base64Data, 'application/x-bittorrent')
const file = new File([blob], fileName, { type: 'application/x-bittorrent' })
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 = (status = 'active') => {
const navigateTaskList = (payload = {}) => {
const { status = 'active' } = payload
router.push({ path: `/task/${status}` }).catch(err => {
console.log(err)
})
@@ -101,7 +168,7 @@ commands.register('application:task-list', navigateTaskList)
commands.register('application:preferences', navigatePreferences)
commands.register('application:about', showAboutPanel)
commands.register('application:new-task', showAddTask)
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)
@@ -116,3 +183,4 @@ 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)
+40
View File
@@ -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'
@@ -13,9 +15,44 @@ 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'))
@@ -58,8 +95,11 @@ function init (config) {
global.app.commands = commands
require('./commands')
global.app.trayWorker = initTrayWorker()
setTimeout(() => {
loading.close()
getCurrentWindow().setHasShadow(true)
}, 400)
}
+26 -18
View File
@@ -1,6 +1,6 @@
import { ADD_TASK_TYPE } from '@shared/constants'
import api from '@/api'
import { getSystemTheme } from '@/utils/native'
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 }, uri = '') {
commit('CHANGE_ADD_TASK_URL', 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)
+9 -6
View File
@@ -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)
+85 -20
View File
@@ -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
}
+51 -76
View File
@@ -1,26 +1,16 @@
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 {
bytesToSize,
getTaskFullPath,
isMagnetTask
getFileNameFromFile,
isMagnetTask,
getSystemMajorVersion
} 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
@@ -33,18 +23,48 @@ export function showItemInFolder (fullPath, { errorMsg }) {
return
}
remote.shell.showItemInFolder(fullPath)
shell.showItemInFolder(fullPath)
})
}
export function openItem (fullPath, { errorMsg }) {
export const openItem = async (fullPath) => {
if (!fullPath) {
return
}
const result = remote.shell.openItem(fullPath)
if (!result && errorMsg) {
Message.error(errorMsg)
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
}
@@ -69,7 +89,9 @@ export function moveTaskFilesToTrash (task) {
access(path, constants.F_OK, (err) => {
console.log(`[Motrix] ${path} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult1 = remote.shell.moveItemToTrash(path)
// Electron >= 12.x
// deleteResult1 = shell.trashItem(path)
deleteResult1 = shell.moveItemToTrash(path)
}
})
@@ -83,83 +105,36 @@ export function moveTaskFilesToTrash (task) {
access(extraFilePath, constants.F_OK, (err) => {
console.log(`[Motrix] ${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult2 = remote.shell.moveItemToTrash(extraFilePath)
// Electron >= 12.x
// deleteResult2 = shell.trashItem(extraFilePath)
deleteResult2 = 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
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
export const openExternal = (url, options) => {
if (!url) {
return
}
remote.shell.openExternal(url, options)
export function isBigSur () {
return is.macOS() && getSystemMajorVersion() >= 20
}
export const delayDeleteTaskFiles = (task, delay) => {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const result = moveTaskFilesToTrash(task)
resolve(result)
} catch (err) {
console.log('[Motrix] batch delay delete task files fail', err)
resolve(false)
reject(err.message)
}
}, delay)
})
+129
View File
@@ -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
}
+68
View File
@@ -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))
}
})
+5
View File
@@ -0,0 +1,5 @@
'use strict'
const Aria2 = require('./lib/Aria2')
module.exports = Aria2
+72
View File
@@ -0,0 +1,72 @@
'use strict'
import { JSONRPCClient } from './JSONRPCClient'
export class Aria2 extends JSONRPCClient {
prefix (str) {
if (!str.startsWith('system.') && !str.startsWith('aria2.')) {
str = 'aria2.' + str
}
return str
}
unprefix (str) {
const suffix = str.split('aria2.')[1]
return suffix || str
}
addSecret (parameters) {
let params = this.secret ? ['token:' + this.secret] : []
if (Array.isArray(parameters)) {
params = params.concat(parameters)
}
return params
}
_onnotification (notification) {
const { method, params } = notification
const event = this.unprefix(method)
if (event !== method) this.emit(event, params)
return super._onnotification(notification)
}
async call (method, ...params) {
return super.call(this.prefix(method), this.addSecret(params))
}
async multicall (calls) {
const multi = [
calls.map(([method, ...params]) => {
return { methodName: this.prefix(method), params: this.addSecret(params) }
})
]
return super.call('system.multicall', multi)
}
async batch (calls) {
return super.batch(
calls.map(([method, ...params]) => [
this.prefix(method),
this.addSecret(params)
])
)
}
async listNotifications () {
const events = await this.call('system.listNotifications')
return events.map((event) => this.unprefix(event))
}
async listMethods () {
const methods = await this.call('system.listMethods')
return methods.map((method) => this.unprefix(method))
}
defaultOptions = Object.assign({}, JSONRPCClient.defaultOptions, {
secure: false,
host: 'localhost',
port: 6800,
secret: '',
path: '/jsonrpc'
})
}
+8
View File
@@ -0,0 +1,8 @@
'use strict'
module.exports = function Deferred () {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
+191
View File
@@ -0,0 +1,191 @@
'use strict'
import { JSONRPCError } from './JSONRPCError'
const Deferred = require('./Deferred')
const promiseEvent = require('./promiseEvent')
const _WebSocket = require('ws')
const _fetch = require('node-fetch')
const EventEmitter = require('events')
const WebSocket = global.WebSocket || _WebSocket
const fetch = global.fetch ? global.fetch.bind(global) : _fetch
export class JSONRPCClient extends EventEmitter {
constructor (options) {
super()
this.deferreds = Object.create(null)
this.lastId = 0
Object.assign(this, this.defaultOptions, options)
}
id () {
return this.lastId++
}
url (protocol) {
return (
protocol +
(this.secure ? 's' : '') +
'://' +
this.host +
':' +
this.port +
this.path
)
}
websocket (message) {
return new Promise((resolve, reject) => {
const cb = (err) => {
if (err) reject(err)
else resolve()
}
this.socket.send(JSON.stringify(message), cb)
if (global.WebSocket && this.socket instanceof global.WebSocket) cb()
})
}
async http (message) {
const response = await fetch(this.url('http'), {
method: 'POST',
body: JSON.stringify(message),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
response
.json()
.then(this._onmessage)
.catch((err) => {
this.emit('error', err)
})
return response
}
_buildMessage (method, params) {
if (typeof method !== 'string') {
throw new TypeError(method + ' is not a string')
}
const message = {
method,
'json-rpc': '2.0',
id: this.id()
}
if (params) Object.assign(message, { params })
return message
}
async batch (calls) {
const message = calls.map(([method, params]) => {
return this._buildMessage(method, params)
})
await this._send(message)
return message.map(({ id }) => {
const { promise } = (this.deferreds[id] = new Deferred())
return promise
})
}
async call (method, parameters) {
const message = this._buildMessage(method, parameters)
await this._send(message)
const { promise } = (this.deferreds[message.id] = new Deferred())
return promise
}
async _send (message) {
this.emit('output', message)
const { socket } = this
return socket && socket.readyState === 1
? this.websocket(message)
: this.http(message)
}
_onresponse ({ id, error, result }) {
const deferred = this.deferreds[id]
if (!deferred) return
if (error) deferred.reject(new JSONRPCError(error))
else deferred.resolve(result)
delete this.deferreds[id]
}
_onrequest ({ method, params }) {
return this.onrequest(method, params)
}
_onnotification ({ method, params }) {
this.emit(method, params)
}
_onmessage = (message) => {
this.emit('input', message)
if (Array.isArray(message)) {
for (const object of message) {
this._onobject(object)
}
} else {
this._onobject(message)
}
}
_onobject (message) {
if (message.method === undefined) this._onresponse(message)
else if (message.id === undefined) this._onnotification(message)
else this._onrequest(message)
}
async open () {
const socket = (this.socket = new WebSocket(this.url('ws')))
socket.onclose = (...args) => {
this.emit('close', ...args)
}
socket.onmessage = (event) => {
let message
try {
message = JSON.parse(event.data)
} catch (err) {
this.emit('error', err)
return
}
this._onmessage(message)
}
socket.onopen = (...args) => {
this.emit('open', ...args)
}
socket.onerror = (...args) => {
this.emit('error', ...args)
}
return promiseEvent(this, 'open')
}
async close () {
const { socket } = this
socket.close()
return promiseEvent(this, 'close')
}
defaultOptions = {
secure: false,
host: 'localhost',
port: 80,
secret: '',
path: '/jsonrpc',
fetch,
WebSocket
}
}
+10
View File
@@ -0,0 +1,10 @@
'use strict'
export class JSONRPCError extends Error {
constructor ({ message, code, data }) {
super(message)
this.code = code
if (data) this.data = data
this.name = this.constructor.name
}
}
+23
View File
@@ -0,0 +1,23 @@
'use strict'
const { inspect } = require('util')
module.exports = (aria2) => {
aria2.on('open', () => {
console.log('aria2', 'OPEN')
})
aria2.on('close', () => {
console.log('aria2', 'CLOSE')
})
aria2.on('input', (m) => {
console.log('aria2', 'IN')
console.log(inspect(m, { depth: null, colors: true }))
})
aria2.on('output', (m) => {
console.log('aria2', 'OUT')
console.log(inspect(m, { depth: null, colors: true }))
})
}
+20
View File
@@ -0,0 +1,20 @@
'use strict'
module.exports = function promiseEvent (target, event) {
return new Promise((resolve, reject) => {
function cleanup () {
target.removeListener(event, onEvent)
target.removeListener('error', onError)
}
function onEvent (data) {
resolve(data)
cleanup()
}
function onError (err) {
reject(err)
cleanup()
}
target.addListener(event, onEvent)
target.addListener('error', onError)
})
}
+2 -1
View File
@@ -4,5 +4,6 @@
"paused": "#737373",
"error": "#FF6157",
"complete": "#2ACB42",
"removed": "#737373"
"removed": "#737373",
"seeding": "#2ACB42"
}
+2
View File
@@ -8,6 +8,7 @@ const userKeys = [
'engine-bin-path',
'engine-max-connection-per-server',
'hide-app-menu',
'keep-seeding',
'keep-window-state',
'last-check-update-time',
'last-sync-tracker-time',
@@ -23,6 +24,7 @@ const userKeys = [
'task-notification',
'theme',
'tracker-source',
'tray-speedometer',
'use-proxy'
]
+73 -10
View File
@@ -29,6 +29,9 @@ export const TASK_STATUS = {
export const ENGINE_RPC_HOST = '127.0.0.1'
export const ENGINE_RPC_PORT = 16800
export const UNKNOWN_PEERID = '%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00'
export const GRAPHIC = '░▒▓█'
/**
* @see https://github.com/ngosang/trackerslist
*/
@@ -37,15 +40,24 @@ export const NGOSANG_TRACKERS_BEST_IP_URL = 'https://raw.githubusercontent.com/n
export const NGOSANG_TRACKERS_ALL_URL = 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt'
export const NGOSANG_TRACKERS_ALL_IP_URL = 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt'
export const NGOSANG_TRACKERS_BEST_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_best.txt'
export const NGOSANG_TRACKERS_BEST_IP_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_best_ip.txt'
export const NGOSANG_TRACKERS_ALL_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt'
export const NGOSANG_TRACKERS_ALL_IP_URL_CDN = 'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all_ip.txt'
/**
* @see https://github.com/XIU2/TrackersListCollection
*/
export const XIU2_TRACKERS_BEST_URL = 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt'
export const XIU2_TRACKERS_ALL_URL = 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt'
export const XIU2_TRACKERS_OTHER_URL = 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/other.txt'
export const XIU2_TRACKERS_HTTP_URL = 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt'
export const XIU2_TRACKERS_BEST_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/TrackersListCollection/best.txt'
export const XIU2_TRACKERS_ALL_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/TrackersListCollection/all.txt'
export const XIU2_TRACKERS_HTTP_URL_CDN = 'https://cdn.jsdelivr.net/gh/XIU2/TrackersListCollection/http.txt'
// For bt-exclude-tracker
export const XIU2_TRACKERS_BLACK_URL = 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/blacklist.txt'
export const XIU2_TRACKERS_BLACK_URL = 'https://cdn.jsdelivr.net/gh/XIU2/TrackersListCollection/blacklist.txt'
export const trackerSourceOptions = [
{
@@ -53,19 +65,43 @@ export const trackerSourceOptions = [
options: [
{
value: NGOSANG_TRACKERS_BEST_URL,
label: 'trackers_best.txt'
label: 'trackers_best.txt',
cdn: false
},
{
value: NGOSANG_TRACKERS_BEST_IP_URL,
label: 'trackers_best_ip.txt'
label: 'trackers_best_ip.txt',
cdn: false
},
{
value: NGOSANG_TRACKERS_ALL_URL,
label: 'trackers_all.txt'
label: 'trackers_all.txt',
cdn: false
},
{
value: NGOSANG_TRACKERS_ALL_IP_URL,
label: 'trackers_all_ip.txt'
label: 'trackers_all_ip.txt',
cdn: false
},
{
value: NGOSANG_TRACKERS_BEST_URL_CDN,
label: 'trackers_best.txt',
cdn: true
},
{
value: NGOSANG_TRACKERS_BEST_IP_URL_CDN,
label: 'trackers_best_ip.txt',
cdn: true
},
{
value: NGOSANG_TRACKERS_ALL_URL_CDN,
label: 'trackers_all.txt',
cdn: true
},
{
value: NGOSANG_TRACKERS_ALL_IP_URL_CDN,
label: 'trackers_all_ip.txt',
cdn: true
}
]
},
@@ -74,15 +110,33 @@ export const trackerSourceOptions = [
options: [
{
value: XIU2_TRACKERS_BEST_URL,
label: 'best.txt'
label: 'best.txt',
cdn: false
},
{
value: XIU2_TRACKERS_ALL_URL,
label: 'all.txt'
label: 'all.txt',
cdn: false
},
{
value: XIU2_TRACKERS_OTHER_URL,
label: 'other.txt'
value: XIU2_TRACKERS_HTTP_URL,
label: 'http.txt',
cdn: false
},
{
value: XIU2_TRACKERS_BEST_URL_CDN,
label: 'best.txt',
cdn: true
},
{
value: XIU2_TRACKERS_ALL_URL_CDN,
label: 'all.txt',
cdn: true
},
{
value: XIU2_TRACKERS_HTTP_URL_CDN,
label: 'http.txt',
cdn: true
}
]
}
@@ -115,3 +169,12 @@ export const LOGIN_SETTING_OPTIONS = {
'--opened-at-login=1'
]
}
export const TRAY_CANVAS_CONFIG = {
WIDTH: 66,
HEIGHT: 16,
ICON_WIDTH: 16,
ICON_HEIGHT: 16,
TEXT_WIDTH: 46,
TEXT_FONT_SIZE: 8
}
+48
View File
@@ -1,31 +1,43 @@
import eleLocaleAr from 'element-ui/lib/locale/lang/ar'
import eleLocaleBg from 'element-ui/lib/locale/lang/bg'
import eleLocaleCa from 'element-ui/lib/locale/lang/ca'
import eleLocaleDe from 'element-ui/lib/locale/lang/de'
import eleLocaleEl from 'element-ui/lib/locale/lang/el'
import eleLocaleEn from 'element-ui/lib/locale/lang/en'
import eleLocaleEs from 'element-ui/lib/locale/lang/es'
import eleLocaleFa from 'element-ui/lib/locale/lang/fa'
import eleLocaleFr from 'element-ui/lib/locale/lang/fr'
import eleLocaleHu from 'element-ui/lib/locale/lang/hu'
import eleLocaleId from 'element-ui/lib/locale/lang/id'
import elelocaleIt from 'element-ui/lib/locale/lang/it'
import eleLocaleJa from 'element-ui/lib/locale/lang/ja'
import eleLocaleKo from 'element-ui/lib/locale/lang/ko'
import eleLocalePl from 'element-ui/lib/locale/lang/pl'
import eleLocalePtBR from 'element-ui/lib/locale/lang/pt-br'
import eleLocaleRo from 'element-ui/lib/locale/lang/ro'
import eleLocaleRu from 'element-ui/lib/locale/lang/ru-RU'
import eleLocaleTr from 'element-ui/lib/locale/lang/tr-TR'
import eleLocaleVi from 'element-ui/lib/locale/lang/vi'
import eleLocaleZhCN from 'element-ui/lib/locale/lang/zh-CN'
import eleLocaleZhTW from 'element-ui/lib/locale/lang/zh-TW'
import eleLocaleUk from 'element-ui/lib/locale/lang/ua'
import appLocaleAr from '@shared/locales/ar'
import appLocaleBg from '@shared/locales/bg'
import appLocaleCa from '@shared/locales/ca'
import appLocaleDe from '@shared/locales/de'
import appLocaleEl from '@shared/locales/el'
import appLocaleEnUS from '@shared/locales/en-US'
import appLocaleEs from '@shared/locales/es'
import appLocaleFa from '@shared/locales/fa'
import appLocaleFr from '@shared/locales/fr'
import appLocaleHu from '@shared/locales/hu'
import appLocaleId from '@shared/locales/id'
import applocaleIt from '@shared/locales/it'
import appLocaleJa from '@shared/locales/ja'
import appLocaleKo from '@shared/locales/ko'
import appLocalePl from '@shared/locales/pl'
import appLocalePtBR from '@shared/locales/pt-BR'
import appLocaleRo from '@shared/locales/ro'
import appLocaleRu from '@shared/locales/ru'
import appLocaleTr from '@shared/locales/tr'
import appLocaleVi from '@shared/locales/vi'
@@ -36,6 +48,12 @@ import appLocaleUk from '@shared/locales/uk'
// Please keep the locale key in alphabetical order.
/* eslint-disable quote-props */
const resources = {
'ar': {
translation: {
...eleLocaleAr,
...appLocaleAr
}
},
'ca': {
translation: {
...eleLocaleCa,
@@ -48,6 +66,12 @@ const resources = {
...appLocaleDe
}
},
'el': {
translation: {
...eleLocaleEl,
...appLocaleEl
}
},
'en-US': {
translation: {
...eleLocaleEn,
@@ -72,12 +96,24 @@ const resources = {
...appLocaleFr
}
},
'hu': {
translation: {
...eleLocaleHu,
...appLocaleHu
}
},
'id': {
translation: {
...eleLocaleId,
...appLocaleId
}
},
'it': {
translation: {
...elelocaleIt,
...applocaleIt
}
},
'ja': {
translation: {
...eleLocaleJa,
@@ -90,12 +126,24 @@ const resources = {
...appLocaleKo
}
},
'pl': {
translation: {
...eleLocalePl,
...appLocalePl
}
},
'pt-BR': {
translation: {
...eleLocalePtBR,
...appLocalePtBR
}
},
'ro': {
translation: {
...eleLocaleRo,
...appLocaleRo
}
},
'ru': {
translation: {
...eleLocaleRu,
+36
View File
@@ -1,13 +1,19 @@
import appLocaleAr from '@shared/locales/ar'
import appLocaleBg from '@shared/locales/bg'
import appLocaleCa from '@shared/locales/ca'
import appLocaleDe from '@shared/locales/de'
import appLocaleEl from '@shared/locales/el'
import appLocaleEnUS from '@shared/locales/en-US'
import appLocaleFa from '@shared/locales/fa'
import appLocaleFr from '@shared/locales/fr'
import appLocaleHu from '@shared/locales/hu'
import appLocaleId from '@shared/locales/id'
import appLocaleIt from '@shared/locales/it'
import appLocaleJa from '@shared/locales/ja'
import appLocaleKo from '@shared/locales/ko'
import appLocalePl from '@shared/locales/pl'
import appLocalePtBR from '@shared/locales/pt-BR'
import appLocaleRo from '@shared/locales/ro'
import appLocaleRu from '@shared/locales/ru'
import appLocaleTr from '@shared/locales/tr'
import appLocaleVi from '@shared/locales/vi'
@@ -18,6 +24,11 @@ import appLocaleUk from '@shared/locales/uk'
// Please keep the locale key in alphabetical order.
/* eslint-disable quote-props */
const resources = {
'ar': {
translation: {
...appLocaleAr
}
},
'ca': {
translation: {
...appLocaleCa
@@ -28,6 +39,11 @@ const resources = {
...appLocaleDe
}
},
'el': {
translation: {
...appLocaleEl
}
},
'en-US': {
translation: {
...appLocaleEnUS
@@ -43,11 +59,21 @@ const resources = {
...appLocaleFr
}
},
'hu': {
translation: {
...appLocaleHu
}
},
'id': {
translation: {
...appLocaleId
}
},
'it': {
translation: {
...appLocaleIt
}
},
'ja': {
translation: {
...appLocaleJa
@@ -58,11 +84,21 @@ const resources = {
...appLocaleKo
}
},
'pl': {
translation: {
...appLocalePl
}
},
'pt-BR': {
translation: {
...appLocalePtBR
}
},
'ro': {
translation: {
...appLocaleRo
}
},
'ru': {
translation: {
...appLocaleRu
+7
View File
@@ -0,0 +1,7 @@
export default {
'engine-version': 'إصدار المحرك',
'license': 'الرخصة',
'about': 'حول',
'release': 'الإصدار',
'support': 'الدعم'
}
+32
View File
@@ -0,0 +1,32 @@
export default {
'task-list': 'قائمة التحميلات',
'add-task': 'إضافة تحميل',
'about': 'حول موتركس',
'preferences': 'التفضيلات...',
'check-for-updates': 'التحقق من وجود تحديثات ...',
'check-updates-now': 'تحقق الآن',
'checking-for-updates': 'جاري التحقق من وجود تحديثات...',
'check-for-updates-title': 'التحقق من وجود تحديثات',
'update-available-message': 'يتوفر إصدار أحدث من موتركس، تحديث الآن؟',
'update-not-available-message': 'لديك أحدث إصدار!',
'update-downloaded-message': 'جاهز للتثبيت...',
'update-error-message': 'حدث خطأ أثناء التحديث',
'engine-damaged-message': 'المحرك متضرر، الرجاء إعادة التثبيت : (',
'engine-missing-message': 'المحرك مفقود، الرجاء إعادة التثبيت : (',
'system-error-title': 'خطأ في النظام',
'system-error-message': 'فشل بدء تشغيل التطبيق: {{message}}',
'hide': 'إخفاء موتركس',
'hide-others': 'إخفاء الآخرين',
'unhide': 'إظهار الكل',
'show': 'إظهار موتركس',
'quit': 'الخروج من موتركس',
'under-development-message': 'عذراً، هذه الميزة قيد التطوير...',
'yes': 'نعم',
'no': 'لا',
'cancel': 'إلغاء',
'submit': 'إرسال',
'gt1d': 'أكثر من يوم',
'hour': 'س',
'minute': 'د',
'second': 'ث'
}
+9
View File
@@ -0,0 +1,9 @@
export default {
'undo': 'تراجع',
'redo': 'إعادة',
'cut': 'قص',
'copy': 'نسخ',
'paste': 'لصق',
'delete': 'حذف',
'select-all': 'تحديد الكل'
}
+7
View File
@@ -0,0 +1,7 @@
export default {
'official-website': 'موقع موتركس',
'manual': 'دليل الاستخدام',
'release-notes': 'ملاحظات الإصدار...',
'report-problem': 'الإبلاغ عن مشكلة',
'toggle-dev-tools': 'تفعيل أدوات المطور'
}
+21
View File
@@ -0,0 +1,21 @@
import about from './about'
import app from './app'
import edit from './edit'
import help from './help'
import menu from './menu'
import preferences from './preferences'
import subnav from './subnav'
import task from './task'
import window from './window'
export default {
about,
app,
edit,
help,
menu,
preferences,
subnav,
task,
window
}

Some files were not shown because too many files have changed in this diff Show More