Compare commits

..

356 Commits

Author SHA1 Message Date
Dr_rOot c17ceb365d chore: upgrade electron-builder 2020-05-03 23:00:41 +08:00
Dr_rOot a1851c6b31 chore: bump version v1.5.6 2020-05-03 22:10:32 +08:00
Dr_rOot 071b0a21c2 chore: bump version v1.5.5 2020-05-03 21:58:39 +08:00
Dr_rOot e25dfb143c Merge pull request #621 from agalwood/hotfix/prepare_release_202004280758
refactor: redesigned the lab page
2020-05-03 21:32:12 +08:00
Dr_rOot 652f04bada chore: bump version 2020-05-03 20:26:06 +08:00
Dr_rOot 5962334f3d feat: new lab page 2020-05-03 20:25:32 +08:00
Dr_rOot f984b175dc refactor: code format 2020-05-03 20:25:05 +08:00
Dr_rOot ad1979417a refactor: engine config 2020-04-30 22:22:48 +08:00
Dr_rOot 16822b9b55 Merge pull request #618 from agalwood/feature/upgrade_aria2_202004302143
feat: upgrade aria2 to v1.35.0
2020-04-30 22:15:34 +08:00
Dr_rOot 36551da19e refactor: arai2 config 2020-04-30 21:46:01 +08:00
Dr_rOot f3426fdc90 chore: upgrade aria2 version to 1.35.0
Compile it with reference to the script of q3aql/aria2-static-builds. The difference from the official version of Aria2 is that the max-connection-per-server is modified to 64.

https://github.com/q3aql/aria2-static-builds
2020-04-30 21:45:46 +08:00
Dr_rOot 94ead296bf docs: update readme 2020-04-30 13:26:40 +08:00
Dr_rOot 22a6f73e51 Merge pull request #616 from agalwood/feature/github_action_202004292314
feat: github action
2020-04-30 11:47:28 +08:00
Dr_rOot 94b4c08da9 fix: travis ci rpm 2020-04-30 11:03:52 +08:00
Dr_rOot 041b7701c2 fix: remove pacman 2020-04-30 00:23:27 +08:00
Dr_rOot c20d9b65ee fix: github action mac build notarize 2020-04-30 00:10:57 +08:00
Dr_rOot 47c5daea81 feat: test github action 2020-04-29 23:15:45 +08:00
Dr_rOot 9676e2a060 fix: remove ci node-sass config 2020-04-28 22:50:52 +08:00
Dr_rOot bb75fa6dcc refactor: change drag select color 2020-04-28 13:28:36 +08:00
Dr_rOot b547ef8f2e refactor: improve shutdown upnp speed 2020-04-28 13:22:20 +08:00
Dr_rOot cc44e21d0e feat: implemented menu pause & resume task 2020-04-28 13:22:20 +08:00
Dr_rOot 20ac050514 Merge pull request #615 from agalwood/hotfix/update_deps_202004271523
chore: update dependencies
2020-04-28 08:27:31 +08:00
Dr_rOot 7eeafb6c93 chore: update deps 2020-04-28 08:15:52 +08:00
Dr_rOot 6fd63c872f fix: windows aria2 cmd
revert forever-monitor version to 1.7.2

https://github.com/foreversd/forever-monitor/commit/93cfbef1c5248eeccf00f4d760c04625476aeb51#commitcomment-37379168
2020-04-28 07:56:54 +08:00
Dr_rOot b8b4f04423 chore: bump version 2020-04-27 23:36:30 +08:00
Dr_rOot e3e2abac6f chore: js code format 2020-04-27 23:33:48 +08:00
Dr_rOot b34f3ceb89 chore: vue code format 2020-04-27 23:30:17 +08:00
Dr_rOot feb07a43e1 chore: upgrade babel & eslint 2020-04-27 23:28:38 +08:00
Dr_rOot 4652672ef4 refactor: replace node-sass to dart-sass 2020-04-27 23:23:45 +08:00
Dr_rOot 8b375c019d refactor: code format 2020-04-27 17:59:45 +08:00
Dr_rOot e232e4af70 fix: upnp config watch 2020-04-27 10:02:12 +08:00
Dr_rOot f57d71cebe Merge pull request #611 from agalwood/feature/upnp_202004222153
feat: UPnP support

Closed #411
2020-04-26 23:08:27 +08:00
Dr_rOot 90daf4cb19 chore: bump version 2020-04-22 22:01:37 +08:00
Dr_rOot d1f759ec44 fix: update task progress info with 2020-04-22 22:00:22 +08:00
Dr_rOot 45ad6dfbda chore: update chrome ua 2020-04-22 21:59:35 +08:00
Dr_rOot 65d45f69fe fix: engine shutdown 2020-04-22 21:59:25 +08:00
Dr_rOot 7e626704ac feat: router upnp mapping 2020-04-22 21:58:53 +08:00
Dr_rOot e0f9dce952 Merge pull request #605 from agalwood/hotfix/app_improve_202004062214
refactor: app improve
Closed #588 #547 #491 #484 #481 #439 #425 #410 #376 #174 #106 #85 #27 #560 #413 #598 #551 #603
2020-04-21 18:01:26 +08:00
Dr_rOot edf627923e fix: after sign hook 2020-04-21 17:47:24 +08:00
Dr_rOot 40f7abcc22 fix: drag select components path at linux 2020-04-21 17:36:13 +08:00
Dr_rOot d837e97d02 fix: dev build html webpack plugin 2020-04-21 17:32:48 +08:00
Dr_rOot 13034b4e90 chore: update deps & bump version to 1.5.x 2020-04-21 17:32:30 +08:00
Dr_rOot b3826032ea refactor: max connection per server 2020-04-20 21:40:31 +08:00
Dr_rOot d5e9ada2c9 chore: i18n batch delete task 2020-04-19 15:32:51 +08:00
Dr_rOot 724a2a7f76 fix: aria2 bt conf 2020-04-18 18:02:07 +08:00
Dr_rOot 64326a11af feat: select all task hotkey 2020-04-18 18:01:26 +08:00
Dr_rOot b00536a0c7 feat: task list drag select & batch delete task 2020-04-18 17:31:11 +08:00
Dr_rOot 8885e63474 feat: drag select component
forked from andi23rosca/drag-select-vue
2020-04-18 16:47:14 +08:00
Dr_rOot 28936fd13e feat: batch remove task api 2020-04-18 16:21:00 +08:00
Dr_rOot 08a419f209 refactor: task progress info 2020-04-18 13:18:30 +08:00
Dr_rOot 8a3391c9f4 fix: restart task option split 2020-04-18 11:26:58 +08:00
Dr_rOot 3777a1f753 refactor: remove task 2020-04-18 11:26:11 +08:00
Dr_rOot b81298d6eb refactor: batch change task option api 2020-04-18 11:20:18 +08:00
Dr_rOot f67b9b8dd6 refactor: task status 2020-04-18 10:38:05 +08:00
Dr_rOot a303afb9a5 refactor: improve max connection per server 2020-04-17 22:44:35 +08:00
Dr_rOot eee07b1b7d chore: update readme features 2020-04-15 23:57:00 +08:00
Dr_rOot a27c49a376 chore: update readme 2020-04-15 21:23:21 +08:00
Dr_rOot fd81dc5194 fix: get random int util 2020-04-13 21:24:21 +08:00
Dr_rOot 394dfdfffc chore: i18n bt listen ports 2020-04-13 21:22:25 +08:00
Dr_rOot 99e9aa8046 feat: advanced preference bt listen ports setting 2020-04-12 22:23:43 +08:00
Dr_rOot ec295b3d72 refactor: is renderer linux mas 2020-04-12 19:57:10 +08:00
Dr_rOot 8208440987 feat: bt listen ports config 2020-04-12 19:52:37 +08:00
Dr_rOot 47426f2d66 refactor: import code format 2020-04-12 15:41:16 +08:00
Dr_rOot ac75a05511 refactor: tray manager 2020-04-12 15:31:23 +08:00
Dr_rOot c3ff1f3b23 refactor: log format 2020-04-11 14:22:15 +08:00
Dr_rOot 0f3157980c refactor: remove outdated trackers 2020-04-10 21:12:59 +08:00
Dr_rOot 36929f9dc5 feat: fetch task item peers api 2020-04-07 11:19:19 +08:00
Dr_rOot 11c507008b refactor: app quit 2020-04-06 22:32:44 +08:00
Dr_rOot 3d66549849 fix: preference diff changed 2020-04-06 22:15:44 +08:00
Dr_rOot 47ca535268 fix: main engine client add catch 2020-04-06 22:15:12 +08:00
Dr_rOot 7d102f9fcb fix: constants duplicate declaration 2020-04-06 22:11:22 +08:00
Dr_rOot edf350adf3 Merge pull request #596 from agalwood/hotfix/task_dir_202004061122
fix: several file path related issues

closed #575
2020-04-06 16:56:03 +08:00
Dr_rOot 651f28e7e8 refactor: add task type constant 2020-04-06 16:36:36 +08:00
Dr_rOot 4d7acb7c0a refactor: app theme constant 2020-04-06 16:31:42 +08:00
Dr_rOot b75e969a01 refactor: move task files to trash 2020-04-06 14:46:55 +08:00
Dr_rOot 0b6f5e7d43 refactor: show item in folder 2020-04-06 14:46:45 +08:00
Dr_rOot a82000a13b refactor: temp disabled prettifyDir 2020-04-06 11:27:44 +08:00
Dr_rOot edee889a1a docs: update travis-ci build status badge url 2020-04-05 23:19:29 +08:00
Dr_rOot afd2fdeabf Merge pull request #594 from agalwood/feature/proxy_setting_20200405227
feat: improve proxy setting
2020-04-05 23:18:43 +08:00
Dr_rOot 34011d7bc8 refactor: move rename utils location 2020-04-05 22:50:30 +08:00
Dr_rOot 6cf6b69f8e chore: i18n add task proxy 2020-04-05 22:49:58 +08:00
Dr_rOot a386c704e4 feat: add task support use proxy 2020-04-05 22:49:40 +08:00
Dr_rOot 0994244b0e chore: i18n no proxy input tip 2020-04-05 22:48:37 +08:00
Dr_rOot 777977e001 feat: advanced preference no proxy setting 2020-04-05 22:48:07 +08:00
Dr_rOot af5ac44dc8 Merge pull request #592 from agalwood/hotfix/update_ci_conf_202004052048
chore: update ci conf
2020-04-05 21:46:02 +08:00
Dr_rOot 23382a9415 chore: update ci conf 2020-04-05 20:50:55 +08:00
Dr_rOot 7b2da0f35b Merge pull request #590 from agalwood/feature/tracker_source_202002091515
feat: sync tracker add more source and support auto sync tracker data daily
2020-04-05 12:36:19 +08:00
Dr_rOot 4037dad582 docs: update readme add install electron guide 2020-03-22 23:32:45 +08:00
Dr_rOot 353ee23cfc feat: preference show last sync tracker time 2020-02-23 22:19:45 +08:00
Dr_rOot 29b0d90bc7 fix: Application showPage fn may cause memory leak
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 ready-to-show listeners added to [BrowserWindow]. Use emitter.setMaxListeners() to increase limit
2020-02-23 12:08:41 +08:00
Jiaqi Gu 073e514f95 feat: Automatically hide window when losing focus (#557)
Automatically hide window when losing focus
2020-02-23 11:59:19 +08:00
Dr_rOot a35fba4820 fix: i18n uk transfer settings 2020-02-22 15:47:28 +08:00
Dr_rOot 828d854eb5 chore: i18n sync tracker 2020-02-22 15:46:04 +08:00
Dr_rOot 5dbe157170 feat: auto sync tracker when app launch 2020-02-21 22:26:22 +08:00
Dr_rOot cdc196c2b0 refactor: save preference config 2020-02-21 22:21:05 +08:00
Dr_rOot 3d26445953 refactor: app updater auto check 2020-02-21 22:15:41 +08:00
Dr_rOot f887878b9e feat: main init engine client 2020-02-21 22:11:07 +08:00
Dr_rOot 908f60df1a feat: advanced preference auto sync tracker 2020-02-21 22:04:45 +08:00
Dr_rOot 56c9b58d87 refactor: advanced preference sync tracker 2020-02-20 19:29:54 +08:00
Dr_rOot d56983d45d refactor: fetch bt tracker data 2020-02-20 19:23:48 +08:00
Dr_rOot 8775e12587 refactor: launcher callback 2020-02-19 20:45:48 +08:00
Dr_rOot df2e01813e feat: add trackers blacklist constant 2020-02-18 17:08:01 +08:00
Dr_rOot d77a6faf36 fix: aria2 bt conf 2020-02-16 13:42:46 +08:00
Dr_rOot 5e735b513d feat: task show connections 2020-02-12 16:12:57 +08:00
Dr_rOot 64f0a3db80 fix: sync old data when tracker source changed 2020-02-10 11:05:38 +08:00
Dr_rOot e470ed00d6 refactor: engine rpc config 2020-02-09 15:51:01 +08:00
Dr_rOot 36a27bae46 feat: preference select sync tracker source 2020-02-09 15:44:55 +08:00
Dr_rOot 5d900806e8 fix: system engine config keys 2020-02-09 15:34:45 +08:00
Dr_rOot ace14c85dc refactor: improve ui style 2020-02-09 15:33:13 +08:00
Dr_rOot 3ec4bd26e6 refactor: update manager last check update time 2020-02-09 15:32:04 +08:00
Dr_rOot b9bc5e6c07 feat: auto remove aria2c unrecognized options 2020-02-09 15:23:00 +08:00
Dr_rOot 021fbc243a refactor: rename & add tracker source 2020-02-09 15:21:55 +08:00
Dr_rOot 8b66f2ea6e refactor: make tracker unique 2020-02-09 15:18:11 +08:00
Dr_rOot 7703c28c6e Merge pull request #544 from agalwood/feature/win_title_bar_202002081321
feat: improve win & linux title bar style

Closes #461
2020-02-08 16:07:08 +08:00
Dr_rOot 26db88ec51 refactor: adjust close btn color for win & linux 2020-02-08 13:23:35 +08:00
Dr_rOot 51b7ea4030 refactor: remove title bar button spacing 2020-02-08 13:22:02 +08:00
Dr_rOot 1e2368dd3b docs: update contribution guide 2020-02-08 12:50:04 +08:00
Dr_rOot f28c3ae5c5 fix: adapt the new api of Electron's TouchBar 2020-02-06 22:22:00 +08:00
Dr_rOot ebdd125d91 Merge pull request #529 from agalwood/feature/proxy_tip_202001311802
feat: Perference advanced panel add proxy tip

close #488
2020-01-31 18:50:14 +08:00
Dr_rOot 9bcc386269 feat: perference advanced proxy tip i18n 2020-01-31 18:16:04 +08:00
Dr_rOot 27c48af278 feat: advanced perference add proxy tip 2020-01-31 18:15:37 +08:00
Dr_rOot 4b4df884dc Merge pull request #528 from agalwood/feature/run_mode_202001311505
feature: Preferences add Run Mode setting

close #345 #350
2020-01-31 16:02:10 +08:00
Dr_rOot 64082ddd70 fix: i18n rename appLocalePtBr to appLocalePtBR 2020-01-31 15:45:12 +08:00
Dr_rOot 188a39d0d2 refactor: i18n rename ru-RU to ru 2020-01-31 15:44:30 +08:00
Dr_rOot 44f8f23a7c feat: run mode i18n 2020-01-31 15:37:56 +08:00
Dr_rOot 32340218e2 refactor: change dock state use DockManager 2020-01-31 15:20:01 +08:00
Dr_rOot 85a8430c3f refactor: ipc change update menu state to command 2020-01-31 15:17:45 +08:00
Dr_rOot e47816e273 refactor: save preference use ipc message 2020-01-31 15:15:55 +08:00
Dr_rOot b7356702fc feat: add DockManager 2020-01-31 15:10:51 +08:00
Dr_rOot a61c857e11 feat: add run mode config 2020-01-31 15:09:31 +08:00
Dr_rOot d3ef30d943 refactor: speed options i18n 2020-01-31 15:07:49 +08:00
Dr_rOot b8d5eb5ac1 fix: add task out i18n 2020-01-31 15:06:14 +08:00
Dr_rOot aeaf0fe9ca Merge pull request #527 from agalwood/feature/improve_responsive_202001311239
feature: improve responsive 202001311239
2020-01-31 14:20:24 +08:00
Dr_rOot 8bebb942ca fix: remove TaskSwitch 2020-01-31 14:11:25 +08:00
Dr_rOot f0e795f00c refactor: code format 2020-01-31 12:49:24 +08:00
Dr_rOot 7d4eec6b02 feat: preference responsive subnav switcher 2020-01-31 12:45:30 +08:00
Dr_rOot fe2dc7a541 refactor: task subnav switcher 2020-01-31 12:42:39 +08:00
Dr_rOot 848b9e78e1 Merge pull request #525 from agalwood/hotfix/fix_open_dialog_202001301122
fix: electron v7 dialog showOpenDialog promisify
2020-01-30 15:14:00 +08:00
Dr_rOot 2fa33f75e5 fix: electron v7 dialog showOpenDialog promisify 2020-01-30 11:25:34 +08:00
Dr_rOot 1f6c2a6ff0 Merge pull request #524 from agalwood/hotfix/upgrade_deps_202001232019
fix: upgrade deps 202001232019
2020-01-30 00:05:09 +08:00
Dr_rOot a8e3c7843c fix: ci node sass 2020-01-29 23:52:04 +08:00
Dr_rOot 8b1ba2425c chore: update ci 2020-01-29 23:37:19 +08:00
Dr_rOot 47acf4fad1 fix: remove select torrent checkbox ellipsis 2020-01-28 13:55:53 +08:00
Dr_rOot 9beb48be92 fix: wrong eslint rule place 2020-01-28 13:55:17 +08:00
Dr_rOot bbe52a2a18 chore: update chrome ua 2020-01-28 13:54:27 +08:00
Dr_rOot 0b73028412 refactor: app info engine title style 2020-01-26 23:21:33 +08:00
Dr_rOot 2311a7ed86 fix: catch vue router NavigationDuplicated error 2020-01-25 02:41:46 +08:00
Dr_rOot 95ed300d1b refactor: nativeTheme.shouldUseDarkColors
systemPreferences.isDarkMode()
**Deprecated:** Should use the new `nativeTheme.shouldUseDarkColors` API.
2020-01-25 02:37:28 +08:00
Dr_rOot dcd2d50c83 refactor: dialog.showMessageBox promisify 2020-01-24 22:17:06 +08:00
Dr_rOot 57ed6c7941 chore: upgrade deps 2020-01-23 21:40:12 +08:00
Dr_rOot 870c4089fb refactor: replace babili with terser 2020-01-23 21:39:00 +08:00
Dr_rOot 1082f9330e refactor: upgrade sass loader 2020-01-23 21:37:15 +08:00
Dr_rOot a7f33869a0 chore: daysUntilLock increase to 60 2020-01-05 17:12:01 +08:00
han 89336dda37 feat: make Motrix layout more responsive (#505) 2020-01-05 17:07:09 +08:00
Dr_rOot 4240dee426 Merge pull request #472 from kant/patch-6
fix: Typos fixed and some translation redone
2019-12-12 16:20:33 +08:00
Dr_rOot 83c1c1340e Merge pull request #471 from kant/patch-5
fix: Typos fixed and some translations redone
2019-12-12 16:20:24 +08:00
Dr_rOot 408e33adb1 Merge pull request #470 from kant/patch-4
fix: Typos and bad translations redone
2019-12-12 16:20:15 +08:00
Dr_rOot d2dde274d3 Merge pull request #469 from kant/patch-2
fix: Typo fixed on line 02
2019-12-12 16:20:03 +08:00
Darío Hereñú 7b27f82f1e Typos fixed and some translation redone 2019-11-11 01:49:12 -03:00
Darío Hereñú 2d32a2848c Typos fixed and some translations redone 2019-11-11 01:39:32 -03:00
Darío Hereñú c128689b2e Typos and translation redone 2019-11-11 01:32:57 -03:00
Darío Hereñú 7702aa6ca1 Typos and bad translations redone 2019-11-11 01:29:28 -03:00
Darío Hereñú 2fb32fa347 Typo fixed on line 02 2019-11-11 01:24:48 -03:00
Dr_rOot 2744db2d75 docs: update readme i18n add ca 2019-11-05 23:15:31 +08:00
Dr_rOot adb060b076 Merge pull request #467 from marcizhu/master
feat: add Catalan translation
2019-11-05 23:07:21 +08:00
marcizhu 561d7608d8 feat: add Catalan translation 2019-11-05 11:53:01 +01:00
Dr_rOot 06d9a95827 Merge pull request #424 from agalwood/hotfix/webpack_config_4node12_201909161132
fix: process is not defined at node 12.x #267
2019-09-16 12:08:32 +08:00
Dr_rOot 9c386c4bdc fix: process is not defined at node 12.x #267
https://github.com/SimulatedGREG/electron-vue/issues/871#issuecomment-490370237
2019-09-16 11:34:12 +08:00
Dr_rOot a1eb48926a Merge pull request #420 from agalwood/feature/custom_tracker_source_201909091524
feat: user config custom tracker source
2019-09-09 16:03:58 +08:00
Dr_rOot c1ed827244 feat: add custom tracker source to user config 2019-09-09 15:49:28 +08:00
Dr_rOot 663e1a2db1 refactor: empty string constant 2019-09-09 15:47:56 +08:00
Dr_rOot c0a2b50b9f Merge pull request #419 from agalwood/hotfix/fix_energy_manager_201909091450
fix: stopPowerSaveBlocker when psbId = 0

closed #418
2019-09-09 15:22:04 +08:00
Dr_rOot a843ae4553 fix: stopPowerSaveBlocker when psbId = 0 2019-09-09 14:49:35 +08:00
Rodolfo Robles d195b23512 feat: Spanish Translation (#385) 2019-08-08 11:16:39 +08:00
Zaoqi f6c84b7e57 docs: add extra/README.md 2019-08-06 10:41:46 +08:00
Dr_rOot 98df9c3f2a fix: magnet and thunder protocol handle 2019-07-22 20:04:36 +08:00
Dr_rOot 7e703eb552 chore: update license copyright 2019-07-21 12:38:34 +08:00
Dr_rOot 87bcac6122 fix: update copyright #365
Closed #365
2019-07-21 12:38:27 +08:00
Dr_rOot 6935a12289 chore: readme i18n add ru 2019-07-13 22:49:40 +08:00
Dr_rOot 39688b667e Merge pull request #358 from bladeaweb/feature/ru
feat: Added Russian localisation.
2019-07-13 22:43:53 +08:00
Alexander Sharkov f174887f9f fix: Removed not requirement changes. 2019-07-13 17:08:37 +03:00
Alexander Sharkov caba086651 fix: Fixed some required changes of language short name. 2019-07-13 16:06:41 +03:00
Dr_rOot b627b38985 Merge pull request #357 from bladeaweb/feature/uk
fix: Fixed spelling issue.
2019-07-13 20:31:12 +08:00
Alexander Sharkov 1c35d72aa5 fix: Fixed spelling issue. 2019-07-13 15:06:17 +03:00
Alexander Sharkov 8ecbfdd66d feat: Added Russian localisation. 2019-07-13 15:03:39 +03:00
Dr_rOot 3be568f6ab Merge pull request #354 from agalwood/hotfix/preferences_changed_201907042031
fix: preferences changed data outdated
2019-07-11 11:35:01 +08:00
Dr_rOot fa29c11c73 Merge pull request #353 from agalwood/feature/shift_delete_201907032126
feat: shift delete task with files
2019-07-10 21:42:00 +08:00
Dr_rOot 6b153d68b4 docs: add tips of first launch in linux 2019-07-08 15:55:18 +08:00
Dr_rOot 852266dc16 refactor: preferences function 2019-07-04 21:09:39 +08:00
Dr_rOot 4170cd3419 refactor: lab preferences 2019-07-04 21:04:34 +08:00
Dr_rOot a15870871f fix: advanced preferences changed 2019-07-04 20:57:24 +08:00
Dr_rOot 571499caad fix: basic preferences changed 2019-07-04 20:37:56 +08:00
Dr_rOot 766c2c26b8 feat: shift trash task auto checked remove files 2019-07-03 21:34:27 +08:00
Dr_rOot 1f27f88aa7 feat: shift delete task auto checked remove files 2019-07-03 21:34:11 +08:00
Dr_rOot e06888e998 chore: add pull request template 2019-07-02 20:21:11 +08:00
Dr_rOot 91a2d0a270 Merge pull request #341 from agalwood/feature/download_outs_201907011638
feat: add task out support rule #326

Closed #326
2019-07-01 20:33:06 +08:00
Dr_rOot 8e3fe4c980 refactor: buildRule operator default PLUS 2019-07-01 20:16:26 +08:00
Dr_rOot f3724107f0 refactor: rule operators 2019-07-01 20:14:02 +08:00
Dr_rOot 580c32a2b9 Merge pull request #340 from agalwood/feature/save_preferences_201906271147
fix: check changed preferences is need restart
2019-07-01 18:56:10 +08:00
Dr_rOot 575712ba9f feat: add task support out rule #326
Out rule sample:
abc_(001+).jpg
xyz_(100-2).html
2019-07-01 18:35:10 +08:00
Dr_rOot 84006f348c feat: add buildOuts util 2019-07-01 18:22:04 +08:00
Dr_rOot a9759441d3 chore: update readme 2019-06-30 10:44:11 +08:00
Dr_rOot 728f8e9019 fix: readme i18n typo #338
closed #338
2019-06-30 10:43:27 +08:00
Dr_rOot e814210896 chore: update getLanguage comments 2019-06-29 00:24:54 +08:00
Dr_rOot a0c6877524 fix: remove useless fallback 2019-06-29 00:24:44 +08:00
Dr_rOot 29b0dd627d Merge pull request #334 from bladeaweb/feature/uk
feat: added Ukrainian localisation.
2019-06-29 00:10:16 +08:00
Alexander Sharkov dc48cad7e8 Added: Ukrainian localisation. 2019-06-28 12:47:26 +03:00
Dr_rOot 35df1c799b fix: check changed preferences is need restart 2019-06-27 15:03:08 +08:00
Dr_rOot 912e661c02 chore: update developer tools tips 2019-06-27 11:32:58 +08:00
Dr_rOot 1a2de2ad88 Merge pull request #323 from agalwood/feature/prepare_v4_201906201052
chore: prepare for release v1.4.x
2019-06-24 00:08:19 +08:00
Dr_rOot e44587035d fix: bump version to 1.4.1 2019-06-23 22:58:59 +08:00
Dr_rOot 43f1adcd84 fix: electron 5.0 performance is low revert to 424 2019-06-22 20:46:29 +08:00
Dr_rOot 8a900459b5 refactor: auto copy changed rpc secret 2019-06-22 20:39:38 +08:00
Dr_rOot d2f8a4599d feat: auto copy rpc url when dice changed secret 2019-06-21 15:46:11 +08:00
Dr_rOot 9159fb36ab chore: bump version to v1.4.0 2019-06-21 14:58:07 +08:00
Dr_rOot 5be05395f5 chore: upgrade dependencies 2019-06-21 14:57:45 +08:00
Dr_rOot fba76df945 fix: change app update url 2019-06-21 14:43:32 +08:00
Dr_rOot 5bb4f259cd fix: set bt-detach-seed-only=true 2019-06-21 14:42:57 +08:00
Dr_rOot 4b093a1a4f fix: increase the number of egine process restarts 2019-06-21 14:41:28 +08:00
Dr_rOot 134d81b205 fix: task stop seeding 2019-06-21 14:40:15 +08:00
Dr_rOot d5fc962bca fix: change task option 2019-06-21 14:40:04 +08:00
Dr_rOot 0d1b76d523 fix: checkTaskIsSeeder util 2019-06-20 15:28:28 +08:00
Dr_rOot 6499ce91b8 refactor: update element ui theme chalk 2019-06-20 11:49:30 +08:00
Dr_rOot 2d7907f218 chore: improve npm scripts postinstall 2019-06-20 11:19:54 +08:00
Dr_rOot 97c1cb0418 fix: change rpc doc link 2019-06-20 11:17:16 +08:00
Dr_rOot be585ef102 refactor: move bt-tracker config manager to file 2019-06-20 10:57:48 +08:00
Dr_rOot 03002221e7 Merge branch 'feature/task_list_201905312258'
# Conflicts:
#	src/renderer/components/Native/EngineClient.vue
2019-06-20 10:38:58 +08:00
Dr_rOot 4b8de781a4 refactor: improve download error message #293 2019-06-19 19:56:07 +08:00
Dr_rOot 8224423d8c refactor: update panel content padding 2019-06-18 21:07:14 +08:00
Dr_rOot 1e05e1e4f3 fix: element ui message link color 2019-06-17 20:27:51 +08:00
Dr_rOot bfd1e1b6fa refactor: removeTaskRecord error complete removed 2019-06-17 19:48:57 +08:00
Dr_rOot 79495cbde5 refactor: auto save session when save preferences 2019-06-16 23:44:49 +08:00
Dr_rOot a93d183988 fix: disabled certificate check 2019-06-16 21:30:24 +08:00
Dr_rOot cf082b20d8 fix: there is no localhost rule in /etc/hosts 2019-06-15 23:09:29 +08:00
Dr_rOot 8110409402 feat: empty addTaskOptions when task added 2019-06-15 20:23:11 +08:00
Dr_rOot 37b446b6f0 feat: add parseHeader util for restart task dialog 2019-06-14 23:22:36 +08:00
Dr_rOot 97801fff32 refactor: task item actions restart & stop seeding 2019-06-14 22:38:07 +08:00
Dr_rOot 4ca4f5e606 refactor: add task to support addTaskOptions 2019-06-14 22:20:34 +08:00
Dr_rOot e9caf1b0b4 refactor: transform add options keys to kebab case 2019-06-13 20:47:33 +08:00
Dr_rOot d67dca00b5 feat: add getTaskOption action 2019-06-12 20:36:03 +08:00
Dr_rOot 038536259e feat: add task restart icon 2019-06-12 20:34:22 +08:00
Dr_rOot ec21cc57bc Merge pull request #314 from theweavrs/patch-1
Add code of conduct for the project
2019-06-12 17:03:28 +08:00
thecodrr 5a42e4014b chore: add code of conduct for the project 2019-06-12 00:15:04 -07:00
Dr_rOot 02d4032283 fix: form-actions background color 2019-06-12 09:28:40 +08:00
Dr_rOot d676dbe213 Merge pull request #309 from theweavrs/master
feat: add background under form-actions in preferences
2019-06-12 09:08:31 +08:00
Dr_rOot 02d83534d1 refactor: check is exist before delete task file 2019-06-11 22:41:07 +08:00
Dr_rOot 8b531c76b8 fix: eslint no-empty-pattern error 2019-06-10 19:35:45 +08:00
Dr_rOot 22f92df299 fix: remove useless lab form.protocols key 2019-06-10 19:02:21 +08:00
Dr_rOot eaf91e15e8 Merge pull request #310 from agalwood/feature/protocol_setting_201906091107
feat: add option to set default protocols client #305
2019-06-10 18:34:05 +08:00
Dr_rOot 4535cb706a fix: setup-protocols-client command logic 2019-06-10 17:47:51 +08:00
Dr_rOot a093f3beca chore: i18n tw protocols 2019-06-10 17:47:12 +08:00
Dr_rOot 287c9038b1 Merge pull request #308 from theweavrs/feature/protocol_setting_201906091107
feat: add option to set default protocols
2019-06-10 17:12:25 +08:00
thecodrr 72aac9c0c6 feat: add background under form-actions 2019-06-10 01:48:20 -07:00
thecodrr cdac88371c refactor: remove change handlers 2019-06-10 00:52:30 -07:00
thecodrr 678bed53dc feat: add localization for protocol settings 2019-06-10 00:49:33 -07:00
thecodrr 1f805c3440 refactor: remove unused localized string 2019-06-10 00:22:31 -07:00
thecodrr c8dcb1023f feat: handle setup-protocols-client command 2019-06-10 00:17:55 -07:00
thecodrr a0e7eb84ca feat: send setup-protocols-client command 2019-06-10 00:03:10 -07:00
thecodrr fdf0bb8320 refactor: move protocol settings to advanced menu 2019-06-09 22:53:21 -07:00
thecodrr e2338757de refactor: merge magnet & thunder protocol handling 2019-06-09 22:07:09 -07:00
Dr_rOot c0cbfb5cb2 refactor: preferences lab protocol client settings 2019-06-10 11:35:48 +08:00
Dr_rOot fddaa72ac6 refactor: protocol manager init setup 2019-06-09 23:09:59 +08:00
Dr_rOot 1e531f166b feat: handle thunder protocol 2019-06-09 23:09:27 +08:00
Dr_rOot 97f54adc49 feat: add protocols to user config 2019-06-09 23:08:30 +08:00
Dr_rOot e6548d5548 Merge pull request #306 from agalwood/feature/rpc_secret_201906071137
feat: preferences add rpc-secret setting #138
2019-06-09 22:54:58 +08:00
Dr_rOot 137fdf2b26 fix: npm add randomatic deps 2019-06-09 22:35:41 +08:00
Dr_rOot 10ef4f7318 feat: i18n preferences rpc-secret 2019-06-09 22:33:52 +08:00
Dr_rOot bdd5e56c2a feat: preferences add rpc-secret setting 2019-06-08 22:53:13 +08:00
Dr_rOot a851ec5830 feat: add icon dice 2019-06-08 22:52:20 +08:00
Dr_rOot 2ae3642c62 feat: add check task is seeder util 2019-06-07 20:07:20 +08:00
Dr_rOot 7b41a9e1c6 docs: update scoop install guide 2019-06-06 12:38:29 +08:00
Dr_rOot 012e5ece00 Merge pull request #301 from agalwood/feature/preferences_layout_201905301105
refactor: improve preferences layout
2019-06-05 14:59:09 +08:00
Dr_rOot db8d3d1aec feat: i18n check for update messages 2019-06-05 12:27:18 +08:00
Dr_rOot 8e35dde082 feat: add check for update at preference #298
closed #298
2019-06-05 12:26:45 +08:00
Dr_rOot 5c2acf12ee feat: tray menu support disable item 2019-06-05 11:58:23 +08:00
Dr_rOot e710ccdb34 feat: add check for update to tray menu 2019-06-05 11:57:37 +08:00
Dr_rOot 5ecc08d66d refactor: clean up useless code 2019-06-04 22:05:51 +08:00
Dr_rOot e7b1a898fa feat: add task history icon 2019-06-03 19:57:04 +08:00
Dr_rOot 93acf1ba63 fix: el-progress status value for element ui 2.9.x 2019-06-02 02:22:13 +08:00
Dr_rOot 1164df87e3 fix: i18n add missing ja translates 2019-06-01 11:47:24 +08:00
Dr_rOot 71232603a5 refactor: rename updateStates to updateMenuStates 2019-06-01 11:44:55 +08:00
Dr_rOot 286013b2f0 refacotr: clean code 2019-05-31 19:42:53 +08:00
Dr_rOot a5f0236661 Merge pull request #296 from agalwood/feature/transfer_speed_setting_201905282346
fix: refactor transfer settings
2019-05-30 20:44:28 +08:00
Dr_rOot 87857eba94 refactor: move auto update setting to advanced 2019-05-30 20:43:45 +08:00
Dr_rOot 3059d1a0b7 refactor: preferences move mock ua to security 2019-05-30 15:47:45 +08:00
Dr_rOot 5029f98153 refactor: preferences form item style 2019-05-30 14:25:56 +08:00
Dr_rOot ce80af7ead fix: increase mo-speedometer z-index 2019-05-30 14:25:23 +08:00
Dr_rOot 6df52988b0 chore: i18n preferences transfer settings 2019-05-29 23:57:42 +08:00
Dr_rOot 6359c729ad fix: horizontal scrollbar height 2019-05-29 23:53:58 +08:00
Dr_rOot 04db94daa6 refactor: i18n calc form label width with locale 2019-05-29 23:50:33 +08:00
Dr_rOot d41d809f00 Merge pull request #295 from gee1k/master
fix: remove the button in the aside panel to drag and drop
2019-05-29 15:25:37 +08:00
Svend efdfae37b5 fix:remove the button in the aside panel to drag and drop 2019-05-29 14:01:30 +08:00
Dr_rOot 1a097c02e3 refactor: transfer speed setting to select options 2019-05-28 23:51:34 +08:00
Dr_rOot 0217b5573a fix: theme merge code error 2019-05-28 23:27:57 +08:00
Dr_rOot 86611604d5 Merge branch 'feature/theme_switch_lang_201905232132'
# Conflicts:
#	src/renderer/components/Preference/Advanced.vue
#	src/renderer/components/Preference/Basic.vue
2019-05-28 23:24:08 +08:00
Dr_rOot 0bd8ab3d11 Merge pull request #294 from agalwood/feature/change_global_options_201905241932
feat: change preferences without relaunch app #24 #172 #197
2019-05-28 23:14:05 +08:00
Dr_rOot e9eadb2eba feat: i18n save preferences message 2019-05-28 22:52:50 +08:00
Dr_rOot 72afd845ba fix: default enable check auto update on macOS 2019-05-28 22:35:19 +08:00
Gary Li 91670fac37 fix: updated en-US translation (#290)
* Updated translation: en-US

* Update: en-US translation
2019-05-27 22:38:58 +08:00
Dr_rOot bc14e5b287 Merge pull request #292 from jjandxa/master
feat: add transfer speed setting
2019-05-27 22:33:11 +08:00
jjandxa 2d01431e7d feat: add transfer speed setting 2019-05-26 22:13:38 +08:00
Dr_rOot 81531ef9fc feat: change preference show message 2019-05-26 19:40:13 +08:00
Dr_rOot 04126363ca refactor: change preference without relaunch app 2019-05-25 17:17:00 +08:00
Dr_rOot bcb07656b9 refactor: rename isSyncTracker to trackerSyncing 2019-05-24 19:49:34 +08:00
Dr_rOot b9cd991fd5 refactor: delayed engine client call 2019-05-24 19:45:56 +08:00
Dr_rOot 1b2b22f039 feat: api add changeGlobalOption 2019-05-24 19:45:01 +08:00
Dr_rOot 170a481d48 feat: add diffConfig util 2019-05-24 19:43:53 +08:00
Dr_rOot 48bf7c9e2e refactor: move theme switcher & lang to basic 2019-05-23 21:46:58 +08:00
Dr_rOot 03cfb013e5 docs: fix cn readme typo 2019-05-22 10:31:43 +08:00
Dr_rOot b4e23d8805 chore: add feature request cn issue template 2019-05-22 10:31:25 +08:00
Dr_rOot 43965c5740 chore: update bug report issue template 2019-05-21 10:56:50 +08:00
Dr_rOot 5c8d2e4ca4 fix: unified scrollbar style 2019-05-20 15:01:44 +08:00
Dr_rOot b51c144ace fix: i18n task code format 2019-05-20 14:58:55 +08:00
Dr_rOot feb839b7fa Merge pull request #273 from agalwood/feature/selective_torrent_201905142048
feat: torrent selective download #253 #229 #211
2019-05-19 16:09:09 +08:00
Dr_rOot fa36397afa fix: i18n add task style 2019-05-19 15:48:53 +08:00
Dr_rOot 583fe0ffe2 feat: i18n select torrent 2019-05-19 15:48:03 +08:00
Dr_rOot 8b3a5fbaa8 fix: select torrent reset 2019-05-19 15:45:39 +08:00
Dr_rOot 85493c263b fix: select torrent layout 2019-05-19 15:45:20 +08:00
Dr_rOot 78e106abe4 fix: add selective torrent list utils 2019-05-19 11:44:49 +08:00
Dr_rOot bbf90e9344 fix: torrent file list dark theme style 2019-05-19 11:44:21 +08:00
Dr_rOot dc30c25a83 refactor: select torrent to support selective 2019-05-19 11:42:47 +08:00
Dr_rOot e1cce4f50c refactor: add task to support selective torrent 2019-05-18 21:11:03 +08:00
Dr_rOot 61654ff0ff refactor: remove add task iLoveEggFeatures 2019-05-17 22:39:27 +08:00
Dr_rOot 0142aedfa5 feat: element dialog & table dark theme style 2019-05-17 23:11:24 +08:00
Dr_rOot 323f85ca40 feat: add video audio image files filter utils 2019-05-17 23:08:47 +08:00
Dr_rOot f35e696c6a feat: add file type icon 2019-05-16 20:05:13 +08:00
Dr_rOot d49cbcdc00 feat: add seed-time to system config 2019-05-16 19:54:35 +08:00
Dr_rOot c85ac4e895 feat: set bt-remove-unselected-file to true 2019-05-15 22:06:05 +08:00
Dr_rOot f831cc51ed fix: add task number input text overflow 2019-05-14 20:51:57 +08:00
Dr_rOot d47eb9ba0f refactor: subnav li style 2019-05-14 20:51:04 +08:00
Dr_rOot 119a374db6 fix: clean subnav code 2019-05-13 19:34:47 +08:00
Dr_rOot d26808e43a Merge pull request #262 from agalwood/feature/open_source_license_201905102203
feat: add open source license link to about panel
2019-05-12 11:36:34 +08:00
Dr_rOot 8cb3e54be2 fix: i18n add missing fa about license translate 2019-05-11 10:38:40 +08:00
Dr_rOot c55902cda7 fix: about pannel add license link 2019-05-10 22:33:16 +08:00
Dr_rOot 426b54201b Merge pull request #258 from agalwood/feature/keep_window_size_201905072013
feat: keep window size & position #232 #224 #48
2019-05-09 16:58:40 +08:00
Dr_rOot 6c032cde2d feat: i18n preference keep window state 2019-05-09 12:23:43 +08:00
Dr_rOot eb42ac6cfd feat: preference basic add keep window state chk 2019-05-08 11:40:49 +08:00
Dr_rOot 90f0a7c5f6 feat: restore window state at init 2019-05-08 00:04:53 +08:00
Dr_rOot 9b61dad2c3 feat: store window state 2019-05-08 00:03:25 +08:00
Dr_rOot 671fb82e6a feat: window manager emit resized, moved, closed 2019-05-07 23:56:38 +08:00
Dr_rOot b8f444ca4d fix: detect thunder resource uris add default val 2019-05-06 23:05:56 +08:00
Dr_rOot 7509442e4e refactor: thunder link tip improve 2019-05-05 12:01:11 +08:00
Dr_rOot ee2e3de782 Merge pull request #246 from agalwood/feature/auto_launch_201905031132
feat: auto open at login #164 #210 #237
2019-05-04 20:18:08 +08:00
Dr_rOot c6dd1e333e docs: add fixUserConfig comment 2019-05-04 20:02:16 +08:00
Dr_rOot fba761a29c feat: auto fix user config open-at-login value
Auto fix user config `open-at-login` value when user delete Motrix in OS startup manage
2019-05-04 11:50:51 +08:00
Dr_rOot 4fb94ae8bb fix: second instance could not restore window #233 2019-05-03 22:21:08 +08:00
Dr_rOot 79721822c2 feat: hide window when app launch at login 2019-05-03 15:40:00 +08:00
Dr_rOot 556f58f6b3 chore: i18n open at login 2019-05-03 15:39:18 +08:00
Dr_rOot e690f15335 feat: preference open at login 2019-05-03 15:20:34 +08:00
Dr_rOot 1876587973 feat: auto launch manager 2019-05-03 15:18:57 +08:00
Dr_rOot b89c651132 refactor: auto check update 2019-05-02 15:37:46 +08:00
zhtw ad9d3ed15a fix: some zh-TW typos (#242)
* Update preferences.js

* Update subnav.js

* Update task.js

* Update task.js
2019-05-01 17:54:17 +08:00
Dr_rOot 6aa362332a feat: add dir class watch 2019-05-01 17:09:49 +08:00
Dr_rOot 18d107c062 feat: detect text direction 2019-05-01 17:03:52 +08:00
Dr_rOot ebeba4663c docs: update readme 2019-04-30 21:01:05 +08:00
FloatingShuYin 9bff4257a8 docs: Add tips on how to install package management tools on Windows (#241)
* Add tips on how to install package management tools on Windows

* Remove extra spaces

* Layout revision
2019-04-30 20:16:41 +08:00
218 changed files with 15820 additions and 15189 deletions
+7 -17
View File
@@ -2,19 +2,11 @@
"comments": false,
"env": {
"main": {
"presets": [
["env", {
"targets": { "node": 7 }
}],
"stage-0"
]
"presets": ["@babel/preset-env"]
},
"renderer": {
"presets": [
["env", {
"modules": false
}],
"stage-0"
"@babel/preset-env"
],
"plugins": [
[
@@ -27,12 +19,7 @@
]
},
"web": {
"presets": [
["env", {
"modules": false
}],
"stage-0"
],
"presets": ["@babel/preset-env"],
"plugins": [
[
"component",
@@ -44,5 +31,8 @@
]
}
},
"plugins": ["transform-runtime"]
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}
+11 -6
View File
@@ -19,9 +19,13 @@ const errorLog = chalk.bgRed.white(' ERROR ') + ' '
const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
const isCI = process.env.CI || false
if (process.env.BUILD_TARGET === 'clean') clean()
else if (process.env.BUILD_TARGET === 'web') web()
else build()
if (process.env.BUILD_TARGET === 'clean') {
clean()
} else if (process.env.BUILD_TARGET === 'web') {
web()
} else {
build()
}
function clean () {
del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
@@ -74,8 +78,9 @@ function pack (config) {
return new Promise((resolve, reject) => {
config.mode = 'production'
webpack(config, (err, stats) => {
if (err) reject(err.stack || err)
else if (stats.hasErrors()) {
if (err) {
reject(err.stack || err)
} else if (stats.hasErrors()) {
let err = ''
stats.toString({
@@ -129,4 +134,4 @@ function greeting () {
})
} else console.log(chalk.yellow.bold('\n lets-build'))
console.log()
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ const { spawn } = require('child_process')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const webpackHotMiddleware = require('webpack-hot-middleware')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
@@ -49,7 +50,7 @@ function startRenderer () {
})
compiler.hooks.compilation.tap('compilation', compilation => {
compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
HtmlWebpackPlugin.getHooks(compilation).afterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' })
cb()
})
+10 -4
View File
@@ -6,8 +6,7 @@ const devMode = process.env.NODE_ENV !== 'production'
const path = require('path')
const { dependencies, build } = require('../package.json')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin');
let mainConfig = {
entry: {
@@ -59,7 +58,15 @@ let mainConfig = {
},
extensions: ['.js', '.json', '.node']
},
target: 'electron-main'
target: 'electron-main',
optimization: {
minimize: !devMode,
minimizer: [
new TerserPlugin({
extractComments: false,
})
],
},
}
/**
@@ -79,7 +86,6 @@ if (devMode) {
*/
if (!devMode) {
mainConfig.plugins.push(
new BabiliWebpackPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
'appId': `"${build.appId}"`
+32 -8
View File
@@ -6,8 +6,7 @@ const devMode = process.env.NODE_ENV !== 'production'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
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')
@@ -52,8 +51,11 @@ let rendererConfig = {
{
loader: 'sass-loader',
options: {
data: '@import "@/components/Theme/Variables.scss";',
includePaths:[__dirname, 'src']
implementation: require('sass'),
prependData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
},
}
]
@@ -66,9 +68,12 @@ let rendererConfig = {
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
indentedSyntax: true,
data: '@import "@/components/Theme/Variables.scss";',
includePaths:[__dirname, 'src']
prependData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
},
}
]
@@ -166,6 +171,18 @@ let rendererConfig = {
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,
@@ -191,7 +208,15 @@ let rendererConfig = {
},
extensions: ['.js', '.vue', '.json', '.css', '.node']
},
target: 'electron-renderer'
target: 'electron-renderer',
optimization: {
minimize: !devMode,
minimizer: [
new TerserPlugin({
extractComments: false,
})
],
},
}
/**
@@ -212,7 +237,6 @@ if (!devMode) {
rendererConfig.devtool = ''
rendererConfig.plugins.push(
new BabiliWebpackPlugin(),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
+43 -6
View File
@@ -6,8 +6,7 @@ const devMode = process.env.NODE_ENV !== 'production'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
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')
@@ -49,7 +48,16 @@ let webConfig = {
use: [
devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
prependData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
},
}
]
},
{
@@ -57,7 +65,17 @@ let webConfig = {
use: [
devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader?indentedSyntax'
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
indentedSyntax: true,
prependData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
},
}
]
},
{
@@ -134,6 +152,18 @@ let webConfig = {
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,
@@ -161,7 +191,15 @@ let webConfig = {
},
extensions: ['.js', '.vue', '.json', '.css']
},
target: 'web'
target: 'web',
optimization: {
minimize: !devMode,
minimizer: [
new TerserPlugin({
extractComments: false,
})
],
},
}
/**
@@ -171,7 +209,6 @@ if (!devMode) {
webConfig.devtool = ''
webConfig.plugins.push(
new BabiliWebpackPlugin(),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
+7
View File
@@ -0,0 +1,7 @@
src/renderer/components/Icons/*.js
src/shared/locales/*
!src/shared/locales/all.js
!src/shared/locales/app.js
!src/shared/locales/index.js
!src/shared/locales/LocalManager.js
+24 -17
View File
@@ -1,27 +1,34 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
node: true
},
extends: 'standard',
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
globals: {
appId: true,
__static: true
},
plugins: [
'html'
],
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'indent': ['error', 2],
'vue/script-indent': ['error', 2, {
'baseIndent': 1
}],
},
overrides: [
{
'files': ['*.vue'],
'rules': {
'indent': 'off'
}
}
]
};
+1
View File
@@ -11,6 +11,7 @@ assignees: ''
Before you feedback, please search for the issues to see if there are similar problems that can solve your problem.
**Please delete the above and the contents of this line, then fill in the feedback form in the following format, Thanks.**
<!-- Windows and Linux versions hide the application menu by default. Please use the keyboard shortcut "Ctrl+Shift+I" to open "Developer Tools" -->
**Describe the bug**
A clear and concise description of what the bug is.
+8 -5
View File
@@ -6,12 +6,14 @@ labels: ''
assignees: ''
---
**反馈之前**
<!--
反馈之前请搜索一下已有 issues 和 帮助文档,看是否有类似问题可以解决你的问题
https://github.com/agalwood/Motrix/issues
http://motrix.app/support
**请删除上面和本行的内容,然后按以下格式填写反馈信息,谢谢**
按以下格式填写反馈信息,谢谢
-->
**错误描述**
清楚简洁地描述错误,方便我们复现之后处理。
@@ -24,11 +26,12 @@ http://motrix.app/support
4. 发现报错
**预期的行为**
清楚简洁地描述期望发生的事情。
清楚简洁地描述期望发生的事情。
**截图**
请添加屏幕截图以帮助解释的问题:
请添加屏幕截图以帮助解释的问题:
打开应用菜单中的「帮助」——「开发者工具」—— 切换到 console,然后**完整**截图。
<!-- Windows 和 Linux 版本默认隐藏了应用菜单,请使用键盘快捷键 Ctrl+Shift+I 打开「开发者工具」 -->
**运行环境**
- 操作系统类型: [如 macOS, Windows, Linux]
@@ -37,4 +40,4 @@ http://motrix.app/support
- 安装包类型:[如 dmg, AppImage]
**更多信息**
添加有关问题的任何其他上下文信息。
补充有关问题的其他信息。
@@ -0,0 +1,28 @@
---
name: 新功能请求
about: 你期望 Motrix 未来添加的新功能
title: ''
labels: enhancement ✨
assignees: ''
---
<!--
反馈之前请搜索一下已有 issues 和 帮助文档,看是否已经有人提交了类似的新功能请求
https://github.com/agalwood/Motrix/issues
http://motrix.app/support
按以下格式填写反馈信息,谢谢
-->
**请描述一下你的新功能请求是否与已知问题有关?**
简明扼要地描述了问题所在。
**描述你想要的解决方案**
简明扼要地描述你想要的解决方案。
**描述你考虑过的替代方案**
简明扼要地描述你考虑过的任何替代解决方案或功能。
**更多信息**
补充有关该新功能的其他信息。
+16
View File
@@ -0,0 +1,16 @@
<!-- You can erase any parts of this template not applicable to your Pull Request. -->
## Description
<!-- Write a brief description of the changes introduced by this PR -->
## Related Issues
<!--
Link to the issue that is fixed by this PR (if there is one)
e.g. Fixes #1234, Addresses #1234, Related to #1234, etc.
-->
### Checklist:
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change?
* [ ] Have you linted your code locally prior to submission?
* [ ] Have you successfully ran app with your changes locally?
+1 -1
View File
@@ -1,7 +1,7 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 30
daysUntilLock: 60
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
+60
View File
@@ -0,0 +1,60 @@
name: Build/release
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
release:
runs-on: ${{ matrix.os }}
# Platforms to build on/for
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
# Only install Snapcraft on Ubuntu
if: startsWith(matrix.os, 'ubuntu')
with:
# Log in to Snap Store
snapcraft_token: ${{ secrets.snapcraft_token }}
- name: Prepare for app notarization (macOS)
if: startsWith(matrix.os, 'macos')
# Import Apple API key for app notarization on macOS
run: |
mkdir -p ~/private_keys/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
build_script_name: 'build:github'
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.github_token }}
# macOS code signing certificate
mac_certs: ${{ secrets.mac_certs }}
mac_certs_password: ${{ secrets.mac_certs_password }}
# 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') }}
env:
# macOS notarization API key
API_KEY_ID: ${{ secrets.api_key_id }}
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
+17 -5
View File
@@ -1,32 +1,44 @@
osx_image: xcode10.1
sudo: required
dist: trusty
language: c
matrix:
jobs:
include:
- os: osx
osx_image: xcode11.3
- os: linux
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
cache:
directories:
- node_modules
- "$HOME/.electron"
- "$HOME/.cache"
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
addons:
apt:
packages:
- libgnome-keyring-dev
- icnsutils
- rpm
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
- nvm install 10
- nvm install 12.14.1
- source ~/.bashrc
- npm install -g xvfb-maybe
- npm install
script:
- npm run release
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
branches:
only:
- master
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at agalwood.net@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+2
View File
@@ -1,5 +1,7 @@
# Motrix 贡献指南
开始贡献之前,确保你已经理解了 [GitHub 的协作流程](https://guides.github.com/introduction/flow/)。
## 🌍 翻译指南
首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://electronjs.org/docs/api/locales)
+2
View File
@@ -1,5 +1,7 @@
# Motrix Contributing Guide
Before you start contributing, make sure you already understand [GitHub flow](https://guides.github.com/introduction/flow/).
## 🌍 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).
+3 -1
View File
@@ -1,4 +1,6 @@
Copyright 2018 Dr_rOot
The MIT License
Copyright 2018-present Dr_rOot
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+29 -7
View File
@@ -8,11 +8,11 @@
## 一款全能的下载工具
[![GitHub release](https://img.shields.io/github/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) [![Build Status](https://travis-ci.org/agalwood/Motrix.svg?branch=master)](https://travis-ci.org/agalwood/Motrix) [![Build status](https://ci.appveyor.com/api/projects/status/l11d5h05xwwcvoux/branch/master?svg=true)](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg)](https://github.com/agalwood/Motrix/releases) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
[![GitHub release](https://img.shields.io/github/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) [![Build Status](https://travis-ci.com/agalwood/Motrix.svg?branch=master)](https://travis-ci.com/agalwood/Motrix) [![Build status](https://ci.appveyor.com/api/projects/status/l11d5h05xwwcvoux/branch/master?svg=true)](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg)](https://github.com/agalwood/Motrix/releases) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
我是个兴趣使然的桌面应用开发者🤓,利用搬砖之余开发了 Motrix。
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链、百度网盘等资源。它的界面简洁易用,希望大家喜欢 👻。
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链、盘等资源。它的界面简洁易用,希望大家喜欢 👻。
✈️ 去 [官网](https://motrix.app/zh-CN) 逛逛 | 📖 查看 [帮助手册](http://motrix.app/support/issues)
@@ -20,9 +20,20 @@ Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链
[GitHub](https://github.com/agalwood/Motrix/releases) 和 [官网](https://motrix.app/zh-CN) 提供了已经编译好的稳定版安装包,当然你也可以自己克隆代码编译打包。
### Windows
建议使用安装包(Motrix-Setup-x.y.z.exe)安装 Motrix 以确保完整的体验,例如关联 torrent 文件,捕获磁力链等。
如果你更喜欢便携版,你可以使用 [scoop](https://github.com/lukesampson/scoop)(需要 Windows 7+,天朝用户可能需要设置 Git 代理)安装最新便携版本的 Motrix。
```bash
scoop bucket add extras
scoop install motrix
```
### macOS
更新:macOS 用户支持 `brew cask` 安装,感谢 [Mitscherlich](https://github.com/Mitscherlich) 的 [PR](https://github.com/Homebrew/homebrew-cask/pull/59494)。
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
@@ -30,7 +41,7 @@ brew update && brew cask install motrix
### Linux
可以下载 AppImage(适用于所有Linux发行版)软件包或 snap 或从源代码构建。
可以下载 AppImage(适用于所有 Linux 发行版)软件包或 snap 或从源代码构建安装 Motrix
构建请阅读 **编译打包** 部分。
@@ -42,16 +53,20 @@ brew update && brew cask install motrix
yay motrix
```
Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能没有创建下载会话文件的权限 (`/var/cache/aria2.session`)。
## ✨ 特性
- 🕹 简洁明了的图形操作界面
- 🦄 支持BT和磁力链任务
- 💾 支持下载百度云盘资源
- ☑️ 支持选择性下载BT部分文件
- 💾 支持下载某盘资源
- 🎛 最高支持 10 个任务同时下载
- 🚀 单任务最高支持 64 线程下载
- 🚥 设置上传/下载限速
- 🕶 模拟用户代理UA
- 🔔 下载完成后通知
- 💻 支持触控栏快捷 (Mac 专享)
- 💻 支持触控栏快捷 (Mac 专享)
- 🤖 常驻系统托盘,操作更加便捷
- 🌑 深色模式
- 🗑 移除任务时可同时删除相关文件
@@ -85,6 +100,10 @@ export ELECTRON_MIRROR='https://npm.taobao.org/mirrors/electron/'
export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
```
> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again
`Electron` 下载安装失败的问题,解决方式请参考 https://github.com/electron/electron/issues/8466#issuecomment-571425574
如果喜欢 [Yarn](https://yarnpkg.com/),也可以使用 `yarn` 安装依赖
### 开发模式
@@ -121,14 +140,17 @@ npm run build
| Key | Name | Status |
|-------|:--------------------|:-------------|
| de | German | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| en-US | English | ✔️ |
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| zh-CN | 简体中文 | ✔️ |
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
+28 -6
View File
@@ -6,11 +6,11 @@
## A full-featured download manager
[![GitHub release](https://img.shields.io/github/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) [![Build Status](https://travis-ci.org/agalwood/Motrix.svg?branch=master)](https://travis-ci.org/agalwood/Motrix) [![Build status](https://ci.appveyor.com/api/projects/status/l11d5h05xwwcvoux/branch/master?svg=true)](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg)](https://github.com/agalwood/Motrix/releases) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
[![GitHub release](https://img.shields.io/github/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) [![Build Status](https://travis-ci.com/agalwood/Motrix.svg?branch=master)](https://travis-ci.com/agalwood/Motrix) [![Build status](https://ci.appveyor.com/api/projects/status/l11d5h05xwwcvoux/branch/master?svg=true)](https://ci.appveyor.com/project/agalwood/motrix/branch/master) [![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg)](https://github.com/agalwood/Motrix/releases) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
English | [简体中文](./README-CN.md)
Motrix is a full-featured download manager that supports downloading HTTP, FTP, BitTorrent, Magnet, Baidu Net Disk, etc.
Motrix is a full-featured download manager that supports downloading HTTP, FTP, BitTorrent, Magnet, etc.
Motrix has a clean and easy to use interface. I hope you will like it 👻.
@@ -20,9 +20,20 @@ Motrix has a clean and easy to use interface. I hope you will like it 👻.
Download from [GitHub Releases](https://github.com/agalwood/Motrix/releases) and install it.
### Windows
It is recommended to install Motrix using the installation package (Motrix-Setup-x.y.z.exe) to ensure a complete experience, such as associating torrent files, capturing magnet links, etc.
If you prefer the portable version, you can use [scoop](https://github.com/lukesampson/scoop) (need Windows 7+) to install Motrix.
```bash
scoop bucket add extras
scoop install motrix
```
### macOS
Update: macOS user support `brew cask` installation, thanks to [PR](https://github.com/Homebrew/homebrew-cask/pull/59494) of [Mitscherlich](https://github.com/Mitscherlich).
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
@@ -30,7 +41,7 @@ brew update && brew cask install motrix
### Linux
You can download the AppImage(for all Linux distributions) package or snap or just build from source code.
You can download the AppImage (for all Linux distributions) package or snap or just build from source code to install Motrix.
Please read the **Build** section.
@@ -42,13 +53,17 @@ Run the following command to install:
yay motrix
```
Motrix may need to run with `sudo` for the first time in Linux because there is no permission to create the download session file (`/var/cache/aria2.session`).
## ✨ Features
- 🕹 Simple and clear user interface
- 🦄 Supports BitTorrent & Magnet
- 💾 Supports downloading Baidu Net Disk
- ☑️ BitTorrent selective download
- 💾 Supports downloading BD Net Disk
- 🎛 Up to 10 concurrent download tasks
- 🚀 Supports 64 threads in a single task
- 🚥 Supports speed limit
- 🕶 Mock User-Agent
- 🔔 Download completed Notification
- 💻 Ready for Touch Bar (Mac only)
@@ -77,6 +92,10 @@ cd Motrix
npm install
```
> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again
`Electron` failed to install correctly, please refer to https://github.com/electron/electron/issues/8466#issuecomment-571425574
If you like [Yarn](https://yarnpkg.com/), you can also use `yarn` to install dependencies.
### Dev Mode
@@ -113,14 +132,17 @@ Translations into versions for other languages are welcome 🧐! Please read the
| Key | Name | Status |
|-------|:--------------------|:-------------|
| de | German | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| en-US | English | ✔️ |
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| zh-CN | 简体中文 | ✔️ |
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
+1 -1
View File
@@ -1,2 +1,2 @@
provider: generic
url: 'https://motrix.app/release/'
url: 'https://dl.motrix.app/release/'
+6 -9
View File
@@ -1,24 +1,17 @@
version: 1.0.{build}
branches:
only:
- master
image: Visual Studio 2017
platform:
- x64
cache:
- node_modules
- '%APPDATA%\npm-cache'
- '%USERPROFILE%\.electron'
- '%USERPROFILE%\AppData\Local\Yarn\cache'
init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 10 x64
- ps: Install-Product node 12.14.1 x64
- git reset --hard HEAD
- npm install
- node --version
@@ -27,3 +20,7 @@ build_script:
- npm run release
test: off
branches:
only:
- master
+94
View File
@@ -0,0 +1,94 @@
// Forked from https://github.com/samuelmeuli/mini-diary/blob/master/scripts/after-pack.js
/**
* Source: https://github.com/patrikx3/redis-ui/blob/master/src/build/after-pack.js
*
* Copyright (c) 2019 Patrik Laszlo / P3X / Corifeus and contributors.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// TODO: Remove script once https://github.com/electron/electron/issues/17972 is solved by
// `electron-builder`
const fs = require('fs')
const { spawn } = require('child_process')
const { chdir } = require('process')
const pkg = require('../package.json')
const binName = `${pkg.name}`.toLowerCase()
const exec = async function exec (cmd, args = []) {
const child = spawn(cmd, args, { shell: true })
redirectOutputFor(child)
await waitFor(child)
}
const redirectOutputFor = child => {
const printStdout = data => {
process.stdout.write(data.toString())
}
const printStderr = data => {
process.stderr.write(data.toString())
}
child.stdout.on('data', printStdout)
child.stderr.on('data', printStderr)
child.once('close', () => {
child.stdout.off('data', printStdout)
child.stderr.off('data', printStderr)
})
}
const waitFor = async function (child) {
return new Promise(resolve => {
child.once('close', () => resolve())
})
}
const linuxTargets = [
'AppImage',
'deb',
'rpm',
'snap'
]
module.exports = async function (context) {
console.warn('after build; disable sandbox')
const isLinux = context.targets.find(
target => linuxTargets.includes(target)
)
if (!isLinux) {
return
}
const originalDir = process.cwd()
const dirname = context.appOutDir
chdir(dirname)
await exec('mv', [binName, binName + '.bin'])
const wrapperScript = `#!/bin/bash
"\${BASH_SOURCE%/*}"/${binName}.bin "$@" --no-sandbox
`
fs.writeFileSync(binName, wrapperScript)
await exec('chmod', ['+x', binName])
chdir(originalDir)
}
+27
View File
@@ -0,0 +1,27 @@
require('dotenv').config()
const { notarize } = require('electron-notarize')
const pkg = require('../package.json')
exports.default = async function (context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') {
return
}
const skipNotarize = process.env.SKIP_NOTARIZE
if (skipNotarize === 'yes') {
console.log('skipping notarize')
return
}
const appBundleId = pkg.build.appId
const appName = context.packager.appInfo.productFilename
return await notarize({
appBundleId,
appPath: `${appOutDir}/${appName}.app`,
ascProvider: process.env.TEAM_ID,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PWD
})
}
+3
View File
@@ -0,0 +1,3 @@
# aria2
Source code: https://github.com/agalwood/aria2
Executable → Regular
+59 -96
View File
@@ -1,108 +1,71 @@
## '#'开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释 ##
## 添加了@和默认启用的选项都是系统需要调用的,请不要随意改动否则可能无法正常运行
###############################
# Motrix macOS Aria2 config file
#
# @see https://aria2.github.io/manual/en/html/aria2c.html
#
###############################
## 文件保存相关 ##
# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
# 此项 OS X 无法使用 $HOME 及 ~/ 设置路径 建议使用 /users/用户名/downloads
#@dir=$HOME/downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#@disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none < falloc ? trunc < prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
# file-allocation=none
# 断点续传
#@continue=true
## 下载连接相关 ##
# 最大同时下载任务数, 运行时可修改, 默认:5
#@max-concurrent-downloads=10
# 同一服务器连接数, 添加时可指定, 默认:1
#@max-connection-per-server=15
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
#@min-split-size=10M
# 单个任务最大线程数, 添加时可指定, 默认:5
#@split=15
# 整体下载速度限制, 运行时可修改, 默认:0
#@max-overall-download-limit=0
# 单个任务下载速度限制, 默认:0
#@max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=128K
# 单个任务上传速度限制, 默认:0
#@max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=false
#运行覆盖已存在文件
#@allow-overwrite=true
#自动重命名
#@auto-file-renaming=true
## 进度保存相关 ##
# 从会话文件中读取下载任务
#@input-file=/Users/Shared/aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
#@save-session=/Users/Shared/aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=30
## RPC相关设置 ##
# 启用RPC, 默认:false
################ RPC ################
# Enable JSON-RPC/XML-RPC server.
enable-rpc=true
# 允许所有来源, 默认:false
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
# 使用本客户端请勿修改此项
# rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
#rpc-secret=token
## BT/PT下载相关 ##
# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
#follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=50101-50109
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
enable-dht6=true
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=50101-50109
################ File system ################
# Save a control file(*.aria2) every SEC seconds.
auto-save-interval=10
# Enable disk cache.
disk-cache=32M
# Specify file allocation method.
file-allocation=falloc
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
save-session-interval=10
################ Task ################
# Exclude seed only downloads when counting concurrent active downloads
bt-detach-seed-only=true
# Verify the peer using certificates specified in --ca-certificate option.
check-certificate=false
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
# without getting a single byte, then force the download to fail.
max-file-not-found=5
# Set number of tries.
max-tries=5
# aria2 does not split less than 2*SIZE byte range.
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
bt-seed-unverified=true
# Set host and port as an entry point to IPv4 DHT network.
dht-entry-point=dht.transmissionbt.com:6881
# Set host and port as an entry point to IPv6 DHT network.
dht-entry-point6=dht.transmissionbt.com:6881
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
enable-dht=true
# Enable IPv6 DHT functionality.
enable-dht6=true
# Enable Peer Exchange extension.
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
#bt-request-peer-speed-limit=50K
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.94
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# Specify the string used during the bitorrent extended handshake for the peer's client version.
peer-agent=Transmission/2.94
# Specify the prefix of peer ID.
peer-id-prefix=-TR2940-
Binary file not shown.
Executable → Regular
+59 -96
View File
@@ -1,108 +1,71 @@
## '#'开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释 ##
## 添加了@和默认启用的选项都是系统需要调用的,请不要随意改动否则可能无法正常运行
###############################
# Motrix Linux Aria2 config file
#
# @see https://aria2.github.io/manual/en/html/aria2c.html
#
###############################
## 文件保存相关 ##
# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
# 此项 OS X 无法使用 $HOME 及 ~/ 设置路径 建议使用 /users/用户名/downloads
#@dir=$HOME/downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#@disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none < falloc ? trunc < prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
file-allocation=trunc
# 断点续传
#@continue=true
## 下载连接相关 ##
# 最大同时下载任务数, 运行时可修改, 默认:5
#@max-concurrent-downloads=10
# 同一服务器连接数, 添加时可指定, 默认:1
#@max-connection-per-server=15
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
#@min-split-size=10M
# 单个任务最大线程数, 添加时可指定, 默认:5
#@split=15
# 整体下载速度限制, 运行时可修改, 默认:0
#@max-overall-download-limit=0
# 单个任务下载速度限制, 默认:0
#@max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=128K
# 单个任务上传速度限制, 默认:0
#@max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=false
#运行覆盖已存在文件
#@allow-overwrite=true
#自动重命名
#@auto-file-renaming=true
## 进度保存相关 ##
# 从会话文件中读取下载任务
#@input-file=/Users/Shared/aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
#@save-session=/Users/Shared/aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=30
## RPC相关设置 ##
# 启用RPC, 默认:false
################ RPC ################
# Enable JSON-RPC/XML-RPC server.
enable-rpc=true
# 允许所有来源, 默认:false
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
# 使用本客户端请勿修改此项
# rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
#rpc-secret=token
## BT/PT下载相关 ##
# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
#follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=50101-50109
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
enable-dht6=true
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=50101-50109
################ File system ################
# Save a control file(*.aria2) every SEC seconds.
auto-save-interval=10
# Enable disk cache.
disk-cache=32M
# Specify file allocation method.
file-allocation=trunc
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
save-session-interval=10
################ Task ################
# Exclude seed only downloads when counting concurrent active downloads
bt-detach-seed-only=true
# Verify the peer using certificates specified in --ca-certificate option.
check-certificate=false
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
# without getting a single byte, then force the download to fail.
max-file-not-found=5
# Set number of tries.
max-tries=5
# aria2 does not split less than 2*SIZE byte range.
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
bt-seed-unverified=true
# Set host and port as an entry point to IPv4 DHT network.
dht-entry-point=dht.transmissionbt.com:6881
# Set host and port as an entry point to IPv6 DHT network.
dht-entry-point6=dht.transmissionbt.com:6881
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
enable-dht=true
# Enable IPv6 DHT functionality.
enable-dht6=true
# Enable Peer Exchange extension.
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
#bt-request-peer-speed-limit=50K
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.94
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# Specify the string used during the bitorrent extended handshake for the peer's client version.
peer-agent=Transmission/2.94
# Specify the prefix of peer ID.
peer-id-prefix=-TR2940-
Binary file not shown.
Executable → Regular
+59 -96
View File
@@ -1,108 +1,71 @@
## '#'开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释 ##
## 添加了@和默认启用的选项都是系统需要调用的,请不要随意改动否则可能无法正常运行
###############################
# Motrix Windows Aria2 config file
#
# @see https://aria2.github.io/manual/en/html/aria2c.html
#
###############################
## 文件保存相关 ##
# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
# 此项 OS X 无法使用 $HOME 及 ~/ 设置路径 建议使用 /users/用户名/downloads
#@dir=$HOME/downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#@disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none < falloc ? trunc < prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
file-allocation=falloc
# 断点续传
#@continue=true
## 下载连接相关 ##
# 最大同时下载任务数, 运行时可修改, 默认:5
#@max-concurrent-downloads=10
# 同一服务器连接数, 添加时可指定, 默认:1
#@max-connection-per-server=15
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
#@min-split-size=10M
# 单个任务最大线程数, 添加时可指定, 默认:5
#@split=15
# 整体下载速度限制, 运行时可修改, 默认:0
#@max-overall-download-limit=0
# 单个任务下载速度限制, 默认:0
#@max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=128K
# 单个任务上传速度限制, 默认:0
#@max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=false
#运行覆盖已存在文件
#@allow-overwrite=true
#自动重命名
#@auto-file-renaming=true
## 进度保存相关 ##
# 从会话文件中读取下载任务
#@input-file=/Users/Shared/aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
#@save-session=/Users/Shared/aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=30
## RPC相关设置 ##
# 启用RPC, 默认:false
################ RPC ################
# Enable JSON-RPC/XML-RPC server.
enable-rpc=true
# 允许所有来源, 默认:false
# Add Access-Control-Allow-Origin header field with value * to the RPC response.
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
# Listen incoming JSON-RPC/XML-RPC requests on all network interfaces.
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
# 使用本客户端请勿修改此项
# rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
#rpc-secret=token
## BT/PT下载相关 ##
# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
#follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=50101-50109
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
enable-dht6=true
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=50101-50109
################ File system ################
# Save a control file(*.aria2) every SEC seconds.
auto-save-interval=10
# Enable disk cache.
disk-cache=32M
# Specify file allocation method.
file-allocation=falloc
# Save error/unfinished downloads to a file specified by --save-session option every SEC seconds.
save-session-interval=10
################ Task ################
# Exclude seed only downloads when counting concurrent active downloads
bt-detach-seed-only=true
# Verify the peer using certificates specified in --ca-certificate option.
check-certificate=false
# If aria2 receives "file not found" status from the remote HTTP/FTP servers NUM times
# without getting a single byte, then force the download to fail.
max-file-not-found=5
# Set number of tries.
max-tries=5
# aria2 does not split less than 2*SIZE byte range.
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
bt-seed-unverified=true
# Set host and port as an entry point to IPv4 DHT network.
dht-entry-point=dht.transmissionbt.com:6881
# Set host and port as an entry point to IPv6 DHT network.
dht-entry-point6=dht.transmissionbt.com:6881
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
# Enable IPv4 DHT functionality. It also enables UDP tracker support.
enable-dht=true
# Enable IPv6 DHT functionality.
enable-dht6=true
# Enable Peer Exchange extension.
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
#bt-request-peer-speed-limit=50K
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.94
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# Specify the string used during the bitorrent extended handshake for the peer's client version.
peer-agent=Transmission/2.94
# Specify the prefix of peer ID.
peer-id-prefix=-TR2940-
Binary file not shown.
+8282 -12951
View File
File diff suppressed because it is too large Load Diff
+91 -72
View File
@@ -1,6 +1,6 @@
{
"name": "Motrix",
"version": "1.3.8",
"version": "1.5.7",
"description": "A full-featured download manager",
"homepage": "https://motrix.app",
"author": {
@@ -17,6 +17,7 @@
"scripts": {
"release": "npm run build --publish onTagOrDraft",
"build": "node .electron-vue/build.js && electron-builder",
"build:github": "node .electron-vue/build.js",
"build:dir": "node .electron-vue/build.js && electron-builder --dir",
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
"build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
@@ -27,11 +28,13 @@
"pack": "npm run pack:main && npm run pack:renderer",
"pack:main": "cross-env NODE_ENV=production webpack --mode production --progress --colors --config .electron-vue/webpack.main.config.js",
"pack:renderer": "cross-env NODE_ENV=production webpack --mode production --progress --colors --config .electron-vue/webpack.renderer.config.js",
"postinstall": "npm run lint:fix"
"postinstall": "electron-builder install-app-deps && npm run lint:fix"
},
"build": {
"productName": "Motrix",
"appId": "net.agalwood.Motrix",
"afterPack": "./build/afterPackHook.js",
"afterSign": "electron-builder-notarize",
"fileAssociations": [
{
"ext": "torrent",
@@ -60,6 +63,12 @@
"schemes": [
"magnet"
]
},
{
"name": "Thunder Protocol",
"schemes": [
"thunder"
]
}
],
"dmg": {
@@ -88,6 +97,7 @@
],
"type": "distribution",
"darkModeSupport": true,
"hardenedRuntime": true,
"extraResources": {
"from": "./extra/darwin/",
"to": "./",
@@ -95,9 +105,6 @@
"**/*"
]
},
"binaries": [
"./release/mac/Motrix.app/Contents/Resources/engine/aria2c"
],
"category": "public.app-category.utilities"
},
"win": {
@@ -139,9 +146,10 @@
"linux": {
"category": "Network",
"target": [
"AppImage",
"deb",
"snap",
"AppImage"
"rpm",
"snap"
],
"extraResources": {
"from": "./extra/linux/",
@@ -151,10 +159,21 @@
]
}
},
"snap": {
"publish": [
{
"provider": "github"
},
{
"provider": "snapStore",
"channel": "edge"
}
]
},
"publish": [
{
"provider": "generic",
"url": "https://motrix.app/release/"
"url": "https://dl.motrix.app/release/"
},
{
"provider": "github"
@@ -162,84 +181,84 @@
]
},
"dependencies": {
"@panter/vue-i18next": "^0.15.0",
"aria2": "^4.0.3",
"axios": "^0.18.0",
"@babel/runtime": "^7.9.2",
"@panter/vue-i18next": "^0.15.2",
"aria2": "^4.1.0",
"axios": "^0.19.2",
"blob-util": "^2.0.2",
"clipboard-polyfill": "^2.8.0",
"electron-debug": "^2.2.0",
"clipboard-polyfill": "^2.8.6",
"electron-debug": "^3.0.1",
"electron-is": "^3.0.0",
"electron-log": "^3.0.5",
"electron-updater": "^4.0.9",
"element-ui": "^2.7.2",
"forever-monitor": "^1.7.1",
"i18next": "^15.0.9",
"lodash": "^4.17.11",
"electron-log": "^4.1.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.1",
"element-ui": "^2.13.1",
"forever-monitor": "1.7.2",
"i18next": "^19.4.4",
"lodash": "^4.17.15",
"nat-api": "^0.1.3",
"node-fetch": "^2.6.0",
"normalize.css": "^8.0.1",
"parse-torrent": "^6.1.2",
"parse-torrent": "^7.1.2",
"randomatic": "^3.1.1",
"svg-innerhtml": "^1.1.0",
"vue": "^2.6.10",
"vue": "^2.6.11",
"vue-electron": "^1.0.6",
"vue-router": "^3.0.6",
"vuex": "^3.1.0",
"vue-router": "^3.1.6",
"vuex": "^3.3.0",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.6.0",
"@vue/cli-plugin-eslint": "^3.6.0",
"@vue/cli-service": "^3.6.0",
"@vue/eslint-config-standard": "^4.0.0",
"ajv": "^6.10.0",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/register": "^7.9.0",
"@vue/eslint-config-standard": "^5.1.2",
"ajv": "^6.12.2",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.26.0",
"babili-webpack-plugin": "^0.1.2",
"cfonts": "^2.4.2",
"chalk": "^2.4.2",
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.1.6",
"css-loader": "^2.1.1",
"del": "^4.1.0",
"cfonts": "^2.8.1",
"chalk": "^4.0.0",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3",
"del": "^5.1.0",
"devtron": "^1.4.0",
"electron": "^4.1.5",
"electron-builder": "^20.39.0",
"electron-devtools-installer": "^2.2.4",
"electron-notarize": "^0.0.5",
"electron-osx-sign": "^0.4.11",
"electron-store": "^2.0.0",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"electron": "^8.2.3",
"electron-builder": "^22.6.0",
"electron-builder-notarize": "^1.1.2",
"electron-devtools-installer": "^3.0.0",
"electron-notarize": "^0.3.0",
"electron-osx-sign": "^0.4.15",
"eslint": "^6.8.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.2",
"eslint-plugin-html": "^4.0.6",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "0.6.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"mini-css-extract-plugin": "0.9.0",
"multispinner": "^0.2.1",
"node-loader": "^0.6.0",
"node-sass": "^4.10.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.0",
"terser-webpack-plugin": "^2.3.6",
"url-loader": "^4.1.0",
"vue-html-loader": "^1.2.4",
"vue-loader": "^15.7.0",
"vue-loader": "^15.9.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1",
"webpack-dev-server": "^3.3.1",
"webpack-hot-middleware": "^2.24.3",
"webpack-merge": "^4.2.1"
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2"
}
}
+327 -42
View File
@@ -3,11 +3,15 @@ import { app, shell, dialog, ipcMain } from 'electron'
import is from 'electron-is'
import { readFile } from 'fs'
import { extname, basename } from 'path'
import { isEmpty } from 'lodash'
import logger from './core/Logger'
import ConfigManager from './core/ConfigManager'
import { setupLocaleManager } from '@/ui/Locale'
import Engine from './core/Engine'
import EngineClient from './core/EngineClient'
import UPnPManager from './core/UPnPManager'
import AutoLaunchManager from './core/AutoLaunchManager'
import UpdateManager from './core/UpdateManager'
import EnergyManager from './core/EnergyManager'
import ProtocolManager from './core/ProtocolManager'
@@ -15,7 +19,11 @@ import WindowManager from './ui/WindowManager'
import MenuManager from './ui/MenuManager'
import TouchBarManager from './ui/TouchBarManager'
import TrayManager from './ui/TrayManager'
import DockManager from './ui/DockManager'
import ThemeManager from './ui/ThemeManager'
import { AUTO_SYNC_TRACKER_INTERVAL, AUTO_CHECK_UPDATE_INTERVAL } from '@shared/constants'
import { checkIsNeedRun } from '@shared/utils'
import { convertTrackerDataToComma, fetchBtTrackerFromSource } from '@shared/utils/tracker'
export default class Application extends EventEmitter {
constructor () {
@@ -36,9 +44,7 @@ export default class Application extends EventEmitter {
this.initTouchBarManager()
this.windowManager = new WindowManager({
userConfig: this.configManager.getUserConfig()
})
this.initWindowManager()
this.engine = new Engine({
systemConfig: this.configManager.getSystemConfig(),
@@ -46,21 +52,40 @@ export default class Application extends EventEmitter {
})
this.startEngine()
this.trayManager = new TrayManager()
this.initEngineClient()
this.initThemeManager()
this.initUPnPManager()
this.autoSyncTracker()
this.trayManager = new TrayManager({
theme: this.configManager.getUserConfig('tray-theme')
})
this.dockManager = new DockManager({
runMode: this.configManager.getUserConfig('run-mode')
})
this.autoLaunchManager = new AutoLaunchManager()
this.energyManager = new EnergyManager()
this.initThemeManager()
this.initUpdaterManager()
this.initProtocolManager()
this.handleCommands()
this.handleEvents()
this.handleIpcMessages()
}
startEngine () {
const self = this
try {
this.engine.start()
} catch (err) {
@@ -69,20 +94,180 @@ export default class Application extends EventEmitter {
type: 'error',
title: this.i18n.t('app.system-error-title'),
message: this.i18n.t('app.system-error-message', { message })
}, () => {
}).then(_ => {
setTimeout(() => {
app.quit()
self.quit()
}, 100)
})
}
}
start (page) {
this.showPage(page)
async stopEngine () {
try {
await this.engineClient.shutdown({ force: true })
} catch (err) {
logger.warn('[Motrix] shutdown engine fail: ', err.message)
} finally {
this.engine.stop()
}
}
showPage (page) {
const win = this.windowManager.openWindow(page)
initEngineClient () {
const port = this.configManager.getSystemConfig('rpc-listen-port')
const secret = this.configManager.getSystemConfig('rpc-secret')
this.engineClient = new EngineClient({
port,
secret
})
}
initUPnPManager () {
this.upnp = new UPnPManager()
this.watchEnableUPnPChange()
this.watchPortsChange()
const enable = this.configManager.getUserConfig('enable-upnp')
if (!enable) {
return
}
this.startUPnPMapping()
}
async startUPnPMapping () {
const btPort = this.configManager.getSystemConfig('listen-port')
const dhtPort = this.configManager.getSystemConfig('dht-listen-port')
const promises = [
this.upnp.map(btPort),
this.upnp.map(dhtPort)
]
try {
await Promise.all(promises)
} catch (e) {
logger.warn('[Motrix] start UPnP mapping fail', e)
}
}
async stopUPnPMapping () {
const btPort = this.configManager.getSystemConfig('listen-port')
const dhtPort = this.configManager.getSystemConfig('dht-listen-port')
const promises = [
this.upnp.unmap(btPort),
this.upnp.unmap(dhtPort)
]
try {
await Promise.all(promises)
} catch (e) {
logger.warn('[Motrix] stop UPnP mapping fail', e)
}
}
watchPortsChange () {
const watchKeys = ['listen-port', 'dht-listen-port']
watchKeys.map((key) => {
this.configManager.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) {
return
}
const promises = [
this.upnp.unmap(oldValue),
this.upnp.map(newValue)
]
try {
await Promise.all(promises)
} catch (e) {
logger.info('[Motrix] change UPnP port mapping failed:', e)
}
})
})
}
watchEnableUPnPChange () {
this.configManager.userConfig.onDidChange('enable-upnp', async (newValue, oldValue) => {
logger.info('[Motrix] detected enable-upnp value change event:', newValue, oldValue)
if (newValue) {
this.startUPnPMapping()
} else {
this.stopUPnPMapping()
}
})
}
async shutdownUPnPManager () {
const enable = this.configManager.getUserConfig('enable-upnp')
if (enable) {
await this.stopUPnPMapping()
}
this.upnp.destroy()
}
autoSyncTracker () {
const enable = this.configManager.getUserConfig('auto-sync-tracker')
const lastTime = this.configManager.getUserConfig('last-sync-tracker-time')
const result = checkIsNeedRun(enable, lastTime, AUTO_SYNC_TRACKER_INTERVAL)
if (!result) {
return
}
setTimeout(() => {
const source = this.configManager.getUserConfig('tracker-source')
fetchBtTrackerFromSource(source).then((data) => {
const tracker = convertTrackerDataToComma(data)
this.savePreference({
system: {
'bt-tracker': tracker
},
user: {
'last-sync-tracker-time': Date.now()
}
})
})
}, 3000)
}
initWindowManager () {
this.windowManager = new WindowManager({
userConfig: this.configManager.getUserConfig()
})
this.windowManager.on('window-resized', (data) => {
this.storeWindowState(data)
})
this.windowManager.on('window-moved', (data) => {
this.storeWindowState(data)
})
this.windowManager.on('window-closed', (data) => {
this.storeWindowState(data)
})
}
storeWindowState (data = {}) {
const enabled = this.configManager.getUserConfig('keep-window-state')
if (!enabled) {
return
}
const state = this.configManager.getUserConfig('window-state', {})
const { page, bounds } = data
const newState = {
...state,
[page]: bounds
}
this.configManager.setUserConfig('window-state', newState)
}
start (page, options = {}) {
const win = this.showPage(page, options)
win.once('ready-to-show', () => {
this.isReady = true
this.emit('ready')
@@ -92,13 +277,21 @@ export default class Application extends EventEmitter {
}
}
showPage (page, options = {}) {
const { openedAtLogin } = options
const autoHideWindow = this.configManager.getUserConfig('auto-hide-window')
return this.windowManager.openWindow(page, {
hidden: openedAtLogin || autoHideWindow
})
}
show (page = 'index') {
this.windowManager.showWindow(page)
}
hide (page) {
if (page) {
this.windowManager.hideWindow(page)
this.windowManager.autoHideWindow(page)
} else {
this.windowManager.hideAllWindow()
}
@@ -112,10 +305,23 @@ export default class Application extends EventEmitter {
this.windowManager.destroyWindow(page)
}
stop () {
this.engine.stop()
this.energyManager.stopPowerSaveBlocker()
this.trayManager.destroy()
async stop () {
try {
await this.shutdownUPnPManager()
this.energyManager.stopPowerSaveBlocker()
this.trayManager.destroy()
await this.stopEngine()
} catch (err) {
logger.warn('[Motrix] stop error: ', err.message)
}
}
async quit () {
await this.stop()
app.exit()
}
sendCommand (command, ...args) {
@@ -160,7 +366,10 @@ export default class Application extends EventEmitter {
if (is.dev() || is.mas()) {
return
}
this.protocolManager = new ProtocolManager()
const protocols = this.configManager.getUserConfig('protocols', {})
this.protocolManager = new ProtocolManager({
protocols
})
}
handleProtocol (url) {
@@ -200,11 +409,11 @@ export default class Application extends EventEmitter {
if (is.mas()) {
return
}
const enable = this.configManager.getUserConfig('auto-check-update')
const lastTime = this.configManager.getUserConfig('last-check-update-time')
this.updateManager = new UpdateManager({
autoCheck: this.configManager.getUserConfig('auto-check-update')
? (new Date().getTime() - this.configManager.getUserConfig('last-check-update-time') > 7 * 24 * 60 * 60 * 1000)
: false,
setCheckTime: this.configManager
autoCheck: checkIsNeedRun(enable, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
})
this.handleUpdaterEvents()
}
@@ -212,6 +421,8 @@ export default class Application extends EventEmitter {
handleUpdaterEvents () {
this.updateManager.on('checking', (event) => {
this.menuManager.updateMenuItemEnabledState('app.check-for-updates', false)
this.trayManager.updateMenuItemEnabledState('app.check-for-updates', false)
this.configManager.setUserConfig('last-check-update-time', Date.now())
})
this.updateManager.on('download-progress', (event) => {
@@ -221,10 +432,12 @@ export default class Application extends EventEmitter {
this.updateManager.on('update-not-available', (event) => {
this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
})
this.updateManager.on('update-downloaded', (event) => {
this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
const win = this.windowManager.getWindow('index')
win.setProgressBar(0)
})
@@ -235,30 +448,52 @@ export default class Application extends EventEmitter {
this.updateManager.on('update-error', (event) => {
this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
this.trayManager.updateMenuItemEnabledState('app.check-for-updates', true)
})
}
relaunch (page = 'index') {
relaunch () {
this.stop()
app.relaunch()
app.exit()
// this.closePage(page)
// if (page === 'index') {
// this.engine.restart()
// }
// setTimeout(() => {
// this.showPage(page)
// }, 500)
}
savePreference (config = {}) {
logger.info('[Motrix] save preference:', config)
const { system, user } = config
if (!isEmpty(system)) {
console.info('[Motrix] main save system config: ', system)
this.configManager.setSystemConfig(system)
this.engineClient.changeGlobalOption(system)
}
if (!isEmpty(user)) {
console.info('[Motrix] main save user config: ', user)
this.configManager.setUserConfig(user)
}
}
handleCommands () {
this.on('application:save-preference', this.savePreference)
this.on('application:relaunch', () => {
this.relaunch()
})
this.on('application:exit', () => {
this.engine.stop()
app.exit()
this.on('application:quit', () => {
this.quit()
})
this.on('application:open-at-login', (openAtLogin) => {
if (is.linux()) {
return
}
if (openAtLogin) {
this.autoLaunchManager.enable()
} else {
this.autoLaunchManager.disable()
}
})
this.on('application:show', (page) => {
@@ -284,7 +519,6 @@ export default class Application extends EventEmitter {
})
this.on('application:change-locale', (locale) => {
logger.info('[Motrix] application:change-locale===>', locale)
this.localeManager.changeLanguageByLocale(locale)
.then(() => {
this.menuManager.setup(locale)
@@ -292,6 +526,29 @@ export default class Application extends EventEmitter {
})
})
this.on('application:toggle-dock', (visible) => {
if (visible) {
this.dockManager.show()
} else {
this.dockManager.hide()
// Hiding the dock icon will trigger the entire app to hide.
this.show()
}
})
this.on('application:auto-hide-window', (hide) => {
if (hide) {
this.windowManager.handleWindowBlur()
} else {
this.windowManager.unbindWindowBlur()
}
})
this.on('application:change-menu-states', (visibleStates, enabledStates, checkedStates) => {
this.menuManager.updateMenuStates(visibleStates, enabledStates, checkedStates)
this.trayManager.updateMenuStates(visibleStates, enabledStates, checkedStates)
})
this.on('application:open-file', (event) => {
dialog.showOpenDialog({
properties: ['openFile'],
@@ -301,8 +558,8 @@ export default class Application extends EventEmitter {
extensions: ['torrent']
}
]
}, (filePaths) => {
if (!filePaths || filePaths.length === 0) {
}).then(({ canceled, filePaths }) => {
if (canceled || filePaths.length === 0) {
return
}
@@ -315,6 +572,14 @@ export default class Application extends EventEmitter {
app.clearRecentDocuments()
})
this.on('application:setup-protocols-client', (protocols) => {
if (is.dev() || is.mas()) {
return
}
logger.info('[Motrix] setup protocols client:', protocols)
this.protocolManager.setup(protocols)
})
this.on('help:official-website', () => {
const url = 'https://motrix.app/'
shell.openExternal(url)
@@ -336,18 +601,38 @@ export default class Application extends EventEmitter {
})
}
handleEvents () {
// this.configManager.systemConfig.onDidAnyChange(() => {
// this.engineClient.changeGlobalOption(this.configManager.getSystemConfig())
// })
this.on('download-status-change', (downloading) => {
this.trayManager.updateTrayByStatus(downloading)
if (downloading) {
this.energyManager.startPowerSaveBlocker()
} else {
this.energyManager.stopPowerSaveBlocker()
}
})
this.on('download-speed-change', (speed) => {
this.dockManager.setBadge(speed)
})
this.on('task-download-complete', (task, path) => {
this.dockManager.openDock(path)
})
}
handleIpcMessages () {
ipcMain.on('command', (event, command, ...args) => {
logger.log('receive command', command, ...args)
logger.log('[Motrix] ipc receive command', command, ...args)
this.emit(command, ...args)
})
ipcMain.on('update-menu-states', (event, visibleStates, enabledStates, checkedStates) => {
this.menuManager.updateStates(visibleStates, enabledStates, checkedStates)
})
ipcMain.on('download-status-change', (event, status) => {
this.trayManager.updateStatus(status)
ipcMain.on('event', (event, eventName, ...args) => {
logger.log('[Motrix] ipc receive event', eventName, ...args)
this.emit(eventName, ...args)
})
}
}
+51 -21
View File
@@ -5,9 +5,12 @@ import is from 'electron-is'
import ExceptionHandler from './core/ExceptionHandler'
import logger from './core/Logger'
import Application from './Application'
import { parseArgvAsUrl, parseArgvAsFile } from './utils'
const EMPTY_STRING = ''
import {
splitArgv,
parseArgvAsUrl,
parseArgvAsFile
} from './utils'
import { EMPTY_STRING } from '@shared/constants'
export default class Launcher extends EventEmitter {
constructor () {
@@ -23,7 +26,7 @@ export default class Launcher extends EventEmitter {
makeSingleInstance (callback) {
// Mac App Store Sandboxed App not support requestSingleInstanceLock
if (is.mas()) {
callback()
callback && callback()
return
}
@@ -33,20 +36,29 @@ export default class Launcher extends EventEmitter {
app.quit()
} else {
app.on('second-instance', (event, argv, workingDirectory) => {
logger.warn('second-instance====>', event, argv, workingDirectory)
global.application.showPage('index')
if (!is.macOS() && argv.length > 1) { // Windows, Linux
if (!is.macOS() && argv.length > 1) {
this.handleAppLaunchArgv(argv)
}
})
callback()
callback && callback()
}
}
init () {
this.exceptionHandler = new ExceptionHandler()
this.openedAtLogin = is.macOS()
? app.getLoginItemSettings().wasOpenedAtLogin
: false
if (process.argv.length > 1) {
this.handleAppLaunchArgv(process.argv)
}
logger.info('[Motrix] openedAtLogin:', this.openedAtLogin)
this.handleAppEvents()
}
@@ -60,11 +72,12 @@ export default class Launcher extends EventEmitter {
/**
* handleOpenUrl
* Event 'open-url' macOS only
* "name": "Motrix Protocol",
* "schemes": ["mo", "motrix"]
*/
handleOpenUrl () {
if (is.mas()) {
if (is.mas() || !is.macOS()) {
return
}
app.on('open-url', (event, url) => {
@@ -77,30 +90,44 @@ export default class Launcher extends EventEmitter {
/**
* handleOpenFile
* Event 'open-file' macOS only
* handle open torrent file
*/
handleOpenFile () {
// macOS
if (is.macOS()) {
app.on('open-file', (event, path) => {
logger.info(`[Motrix] open-file: ${path}`)
event.preventDefault()
this.file = path
this.sendFileToApplication()
})
} else if (process.argv.length > 1) { // Windows, Linux
this.handleAppLaunchArgv(process.argv)
if (!is.macOS()) {
return
}
app.on('open-file', (event, path) => {
logger.info(`[Motrix] open-file: ${path}`)
event.preventDefault()
this.file = path
this.sendFileToApplication()
})
}
/**
* handleAppLaunchArgv
* For Windows, Linux
* @param {array} argv
*/
handleAppLaunchArgv (argv) {
const file = parseArgvAsFile(argv)
logger.info('[Motrix] handleAppLaunchArgv:', argv)
// args: array, extra: map
const { args, extra } = splitArgv(argv)
logger.info('[Motrix] split argv args:', args)
logger.info('[Motrix] split argv extra:', extra)
if (extra['--opened-at-login'] === '1') {
this.openedAtLogin = true
}
const file = parseArgvAsFile(args)
if (file) {
this.file = file
this.sendFileToApplication()
}
const url = parseArgvAsUrl(argv)
const url = parseArgvAsUrl(args)
if (url) {
this.url = url
this.sendUrlToApplication()
@@ -125,7 +152,10 @@ export default class Launcher extends EventEmitter {
app.on('ready', () => {
global.application = new Application()
global.application.start('index')
const { openedAtLogin } = this
global.application.start('index', {
openedAtLogin
})
global.application.on('ready', () => {
this.sendUrlToApplication()
+3 -3
View File
@@ -1,5 +1,5 @@
export default {
'darwin': 'aria2c',
'win32': 'aria2c.exe',
'linux': 'aria2c'
darwin: 'aria2c',
win32: 'aria2c.exe',
linux: 'aria2c'
}
+2 -2
View File
@@ -6,12 +6,12 @@ export default {
title: 'Motrix',
width: 1024,
height: 768,
minWidth: 840,
minWidth: 400,
minHeight: 420,
// backgroundColor: '#FFFFFF',
transparent: !is.windows()
},
bindCloseToHide: true,
url: is.dev() ? `http://localhost:9080` : `file://${__dirname}/index.html`
url: is.dev() ? 'http://localhost:9080' : `file://${__dirname}/index.html`
}
}
+1
View File
@@ -1,3 +1,4 @@
/* eslint quote-props: ["error", "always"] */
export default {
'task-list': 'application:task-list',
'new-task': 'application:new-task',
+35
View File
@@ -0,0 +1,35 @@
import { app } from 'electron'
export default class AutoLaunchManager {
enable () {
return new Promise((resolve, reject) => {
const enabled = app.getLoginItemSettings().openAtLogin
if (enabled) {
resolve()
}
app.setLoginItemSettings({
openAtLogin: true,
// For Windows
args: [
'--opened-at-login=1'
]
})
resolve()
})
}
disable () {
return new Promise((resolve, reject) => {
app.setLoginItemSettings({ openAtLogin: false })
resolve()
})
}
isEnabled () {
return new Promise((resolve, reject) => {
const enabled = app.getLoginItemSettings().openAtLogin
resolve(enabled)
})
}
}
+82 -56
View File
@@ -1,12 +1,23 @@
import { app } from 'electron'
import is from 'electron-is'
import Store from 'electron-store'
import {
getDhtPath,
getLogPath,
getSessionPath,
getUserDownloadsPath
getUserDownloadsPath,
getMaxConnectionPerServer
} from '../utils/index'
import {
APP_RUN_MODE,
APP_THEME,
EMPTY_STRING,
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL,
IP_VERSION
} from '@shared/constants'
import { separateConfig } from '@shared/utils'
export default class ConfigManager {
constructor () {
@@ -26,74 +37,43 @@ export default class ConfigManager {
* https://aria2.github.io/manual/en/html/aria2c.html
*
* Best bt trackers
* https://github.com/ngosang/trackerslist
* @see https://github.com/ngosang/trackerslist
*
* @see https://github.com/XIU2/TrackersListCollection
*/
initSystemConfig () {
this.systemConfig = new Store({
name: 'system',
/* eslint-disable quote-props */
defaults: {
'all-proxy': '',
'allow-overwrite': true,
'all-proxy': EMPTY_STRING,
'allow-overwrite': false,
'auto-file-renaming': true,
'bt-tracker': [
'udp://62.138.0.158:6969/announce',
'udp://188.241.58.209:6969/announce',
'udp://188.241.58.209:6969/announce',
'udp://208.83.20.20:6969/announce',
'udp://151.80.120.115:2710/announce',
'udp://185.225.17.100:1337/announce',
'udp://151.80.120.113:2710/announce',
'udp://62.210.88.151:1337/announce',
'http://176.113.71.19:6961/announce',
'http://104.27.134.253:8080/announce',
'udp://5.2.79.219:1337/announce',
'udp://91.216.110.52:451/announce',
'udp://5.206.58.23:6969/announce',
'udp://159.100.245.181:6969/announce',
'udp://5.2.79.22:6969/announce',
'udp://176.31.241.153:80/announce',
'udp://95.211.168.204:2710/announce',
'udp://188.246.227.212:80/announce',
'udp://51.38.184.185:6969/announce',
'udp://51.15.40.114:80/announce',
'udp://tracker.coppersurfer.tk:6969/announce',
'udp://tracker.open-internet.nl:6969/announce',
'udp://tracker.leechers-paradise.org:6969/announce',
'udp://exodus.desync.com:6969/announce',
'udp://tracker.internetwarriors.net:1337/announce',
'udp://9.rarbg.to:2710/announce',
'udp://9.rarbg.me:2710/announce',
'udp://tracker.opentrackr.org:1337/announce',
'http://tracker3.itzmx.com:6961/announce',
'http://tracker1.itzmx.com:8080/announce',
'udp://open.demonii.si:1337/announce',
'udp://tracker.torrent.eu.org:451/announce',
'udp://tracker.tiny-vps.com:6969/announce',
'udp://tracker.cyberia.is:6969/announce',
'udp://denis.stalker.upeer.me:6969/announce',
'udp://thetracker.org:80/announce',
'udp://bt.xxx-tracker.com:2710/announce',
'udp://open.stealth.si:80/announce',
'udp://tracker.port443.xyz:6969/announce',
'udp://ipv4.tracker.harry.lu:80/announce'
].join(','),
'bt-tracker': EMPTY_STRING,
'continue': true,
'dht-file-path': getDhtPath(4),
'dht-file-path6': getDhtPath(6),
'dht-file-path': getDhtPath(IP_VERSION.V4),
'dht-file-path6': getDhtPath(IP_VERSION.V6),
'dht-listen-port': 26701,
'dir': getUserDownloadsPath(),
'listen-port': 21301,
'max-concurrent-downloads': 5,
'max-connection-per-server': is.macOS() ? 64 : 16,
'max-connection-per-server': getMaxConnectionPerServer(),
'max-download-limit': 0,
'max-overall-download-limit': 0,
'max-overall-upload-limit': '128K',
'max-overall-upload-limit': '256K',
'min-split-size': '1M',
'no-proxy': EMPTY_STRING,
'pause': true,
'rpc-listen-port': 16800,
'rpc-secret': '',
'split': 16,
'rpc-secret': EMPTY_STRING,
'seed-ratio': 1,
'seed-time': 60,
'split': 128,
'user-agent': 'Transmission/2.94'
}
/* eslint-enable quote-props */
})
this.fixSystemConfig()
}
initUserConfig () {
@@ -107,22 +87,68 @@ export default class ConfigManager {
// enum: ['auto', 'light', 'dark']
// }
// },
/* eslint-disable quote-props */
defaults: {
'all-proxy-backup': '',
'auto-check-update': false,
'all-proxy-backup': EMPTY_STRING,
'auto-check-update': is.macOS(),
'auto-hide-window': false,
'auto-sync-tracker': true,
'enable-upnp': true,
'engine-max-connection-per-server': getMaxConnectionPerServer(),
'hide-app-menu': is.windows() || is.linux(),
'keep-window-state': false,
'last-check-update-time': 0,
'last-sync-tracker-time': 0,
'locale': app.getLocale(),
'log-path': getLogPath(),
'new-task-show-downloading': true,
'open-at-login': false,
'protocols': { 'magnet': true, 'thunder': false },
'resume-all-when-app-launched': false,
'run-mode': APP_RUN_MODE.STANDARD,
'session-path': getSessionPath(),
'task-notification': true,
'theme': 'auto',
'theme': APP_THEME.AUTO,
'tracker-source': [
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL
],
'tray-theme': APP_THEME.AUTO,
'update-channel': 'latest',
'use-proxy': false
'use-proxy': false,
'window-state': {}
}
/* eslint-enable quote-props */
})
this.fixUserConfig()
}
fixSystemConfig () {
// Remove aria2c unrecognized options
const { others } = separateConfig(this.systemConfig.store)
if (!others) {
return
}
Object.keys(others).forEach(key => {
this.systemConfig.delete(key)
})
}
fixUserConfig () {
// Fix the value of open-at-login when the user delete
// the Motrix self-starting item through startup management.
const openAtLogin = app.getLoginItemSettings().openAtLogin
if (this.getUserConfig('open-at-login') !== openAtLogin) {
this.setUserConfig('open-at-login', openAtLogin)
}
if (this.getUserConfig('tracker-source').length === 0) {
this.setUserConfig('tracker-source', [
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL
])
}
}
getSystemConfig (key, defaultValue) {
+7 -5
View File
@@ -1,6 +1,8 @@
import { powerSaveBlocker } from 'electron'
let psbId = null
import logger from './Logger'
let psbId
export default class EnergyManager {
startPowerSaveBlocker () {
if (psbId && powerSaveBlocker.isStarted(psbId)) {
@@ -8,16 +10,16 @@ export default class EnergyManager {
}
psbId = powerSaveBlocker.start('prevent-app-suspension')
console.log('startPowerSaveBlocker===>', psbId)
logger.info('[Motrix] start power save blocker:', psbId)
}
stopPowerSaveBlocker () {
if (!psbId || !powerSaveBlocker.isStarted(psbId)) {
if (typeof psbId === 'undefined' || !powerSaveBlocker.isStarted(psbId)) {
return
}
powerSaveBlocker.stop(psbId)
console.log('stopPowerSaveBlocker===>', psbId)
psbId = null
logger.info('[Motrix] stop power save blocker:', psbId)
psbId = undefined
}
}
+21 -22
View File
@@ -1,10 +1,9 @@
'use strict'
import { app } from 'electron'
import is from 'electron-is'
import { existsSync } from 'fs'
import { resolve, join } from 'path'
import forever from 'forever-monitor'
import logger from './Logger'
import { getI18n } from '@/ui/Locale'
import {
@@ -37,16 +36,16 @@ export default class Engine {
throw new Error(this.i18n.t('app.engine-damaged-message'))
}
let binPath = join(basePath, `/engine/${binName}`)
const binPath = join(basePath, `/engine/${binName}`)
const binIsExist = existsSync(binPath)
if (!binIsExist) {
logger.error('[Motrix] engine bin is not exist===>', binPath)
logger.error('[Motrix] engine bin is not exist:', binPath)
throw new Error(this.i18n.t('app.engine-missing-message'))
}
let confPath = join(basePath, '/engine/aria2.conf')
const confPath = join(basePath, '/engine/aria2.conf')
let sessionPath = this.userConfig['session-path'] || getSessionPath()
const sessionPath = this.userConfig['session-path'] || getSessionPath()
const sessionIsExist = existsSync(sessionPath)
let result = [`${binPath}`, `--conf-path=${confPath}`, `--save-session=${sessionPath}`]
@@ -62,9 +61,9 @@ export default class Engine {
start () {
const sh = this.getStartSh()
logger.info('[Motrix] Engine start sh===>', sh)
logger.info('[Motrix] Engine start sh:', sh)
this.instance = forever.start(sh, {
max: 10,
max: is.dev() ? 1 : 100,
parser: function (command, args) {
return {
command: command,
@@ -75,30 +74,30 @@ export default class Engine {
})
const { child } = this.instance
logger.info('[Motrix] Engine pid===>', child.pid)
logger.info('[Motrix] Engine pid:', child.pid)
this.instance.on('error', (err) => {
logger.info(`[Motrix] Engine error===> ${err}`)
logger.info(`[Motrix] Engine error: ${err}`)
})
this.instance.on('start', function (process, data) {
logger.info(`[Motrix] Engine started===>`)
logger.info('[Motrix] Engine started')
})
this.instance.on('stop', function (process) {
logger.info(`[Motrix] Engine stopped===>`)
logger.info('[Motrix] Engine stopped')
})
this.instance.on('restart', function (forever) {
logger.info(`[Motrix] Engine exit===>`)
})
// this.instance.on('restart', function (forever) {
// logger.info(`[Motrix] Engine exit:`)
// })
this.instance.on('exit:code', function (code) {
logger.info(`[Motrix] Engine exit===> ${code}`)
})
// this.instance.on('exit:code', function (code) {
// logger.info(`[Motrix] Engine exit: ${code}`)
// })
// this.instance.on('stderr', (data) => {
// logger.info(`[Motrix] Engine stderr===> ${data}`)
// logger.info(`[Motrix] Engine stderr: ${data}`)
// })
}
@@ -113,10 +112,10 @@ export default class Engine {
stop () {
const { pid } = this.instance.child
try {
logger.info('[Motrix] Engine stopping===>')
logger.info('[Motrix] Engine stopping')
this.instance.stop()
} catch (err) {
logger.error('[Motrix] Engine stop fail===>', err.message)
logger.error('[Motrix] Engine stop fail:', err.message)
this.forceStop(pid)
} finally {
}
@@ -128,7 +127,7 @@ export default class Engine {
process.kill(pid)
}
} catch (err) {
logger.warn('[Motrix] Engine forceStop fail===>', err)
logger.warn('[Motrix] Engine force stop fail:', err)
}
}
+70
View File
@@ -0,0 +1,70 @@
'use strict'
import Aria2 from 'aria2'
import logger from './Logger'
import {
compactUndefined,
formatOptionsForEngine
} from '@shared/utils'
import {
ENGINE_RPC_HOST,
ENGINE_RPC_PORT,
EMPTY_STRING
} from '@shared/constants'
const defaults = {
host: ENGINE_RPC_HOST,
port: ENGINE_RPC_PORT,
secret: EMPTY_STRING
}
export default class EngineClient {
static instance = null
static client = null
constructor (options = {}) {
this.options = {
...defaults,
...options
}
this.init()
}
init () {
this.connect()
}
connect () {
logger.info('[Motrix] main engine client connect', this.options)
const { host, port, secret } = this.options
this.client = new Aria2({
host,
port,
secret
})
}
async call (method, ...args) {
return this.client.call(method, ...args).catch((err) => {
logger.warn('[Motrix] call client fail:', err.message)
})
}
async changeGlobalOption (options) {
logger.info('[Motrix] change engine global option:', options)
const args = formatOptionsForEngine(options)
return this.call('changeGlobalOption', args)
}
async shutdown (options = {}) {
const { force = false } = options
const { secret } = this.options
const method = force ? 'forceShutdown' : 'shutdown'
const args = compactUndefined([secret])
return this.call(method, ...args)
}
}
+1
View File
@@ -1,5 +1,6 @@
import { app, dialog } from 'electron'
import is from 'electron-is'
import logger from './Logger'
const defaults = {
+1 -1
View File
@@ -2,7 +2,7 @@ import is from 'electron-is'
import logger from 'electron-log'
logger.transports.file.level = is.production() ? 'warn' : 'silly'
logger.info('Logger init')
logger.info('[Motrix] Logger init')
logger.warn('[Motrix] Logger init')
export default logger
+32 -15
View File
@@ -1,35 +1,53 @@
import { EventEmitter } from 'events'
import { app } from 'electron'
import logger from './Logger'
import protocolMap from '../configs/protocol'
import { ADD_TASK_TYPE } from '@shared/constants'
export default class ProtocolManager extends EventEmitter {
constructor (options = {}) {
super()
this.options = options
// package.json:build.protocols[].schemes[]
// options.protocols: { 'magnet': true, 'thunder': false }
this.protocols = {
mo: true,
motrix: true,
...options.protocols
}
this.init()
}
init () {
// package.json:build.protocols[].schemes[]
if (!app.isDefaultProtocolClient('mo')) {
app.setAsDefaultProtocolClient('mo')
}
if (!app.isDefaultProtocolClient('motrix')) {
app.setAsDefaultProtocolClient('motrix')
}
if (!app.isDefaultProtocolClient('magnet')) {
app.setAsDefaultProtocolClient('magnet')
}
const { protocols } = this
this.setup(protocols)
}
setup (protocols) {
Object.keys(protocols).forEach((protocol) => {
const enabled = protocols[protocol]
if (enabled) {
if (!app.isDefaultProtocolClient(protocol)) {
app.setAsDefaultProtocolClient(protocol)
}
} else {
app.removeAsDefaultProtocolClient(protocol)
}
})
}
handle (url) {
logger.info(`[Motrix] protocol url: ${url}`)
if (url.toLowerCase().startsWith('magnet:')) {
return this.handleMagnetProtocol(url)
if (
url.toLowerCase().startsWith('magnet:') ||
url.toLowerCase().startsWith('thunder:')
) {
return this.handleMagnetAndThunderProtocol(url)
}
if (
@@ -40,13 +58,12 @@ export default class ProtocolManager extends EventEmitter {
}
}
handleMagnetProtocol (url) {
handleMagnetAndThunderProtocol (url) {
if (!url) {
return
}
logger.error(`[Motrix] handleMagnetProtocol url: ${url}`)
global.application.sendCommandToAll('application:new-task', 'uri', url)
global.application.sendCommandToAll('application:new-task', ADD_TASK_TYPE.URI, url)
}
handleMoProtocol (url) {
+84
View File
@@ -0,0 +1,84 @@
import NatAPI from 'nat-api'
import logger from './Logger'
let client = null
const mappingStatus = {}
export default class UPnPManager {
constructor (options = {}) {
this.options = {
...options
}
}
init () {
if (client) {
return
}
client = new NatAPI()
}
map (port) {
this.init()
return new Promise((resolve, reject) => {
logger.info('[Motrix] UPnPManager port mapping: ', port)
if (!port) {
reject(new Error('[Motrix] port was not specified'))
return
}
client.map(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err)
reject(err.message)
return
}
mappingStatus[port] = true
logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
resolve()
})
})
}
unmap (port) {
this.init()
return new Promise((resolve, reject) => {
logger.info('[Motrix] UPnPManager port unmapping: ', port)
if (!port) {
reject(new Error('[Motrix] port was not specified'))
return
}
if (!mappingStatus[port]) {
resolve()
return
}
client.unmap(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
reject(err.message)
return
}
logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
mappingStatus[port] = false
resolve()
})
})
}
destroy () {
if (!client) {
return
}
client.destroy()
client = null
}
}
+9 -7
View File
@@ -1,8 +1,9 @@
import { EventEmitter } from 'events'
import { resolve } from 'path'
import { dialog } from 'electron'
import is from 'electron-is'
import { autoUpdater } from 'electron-updater'
import { resolve } from 'path'
import logger from './Logger'
import { getI18n } from '@/ui/Locale'
@@ -43,13 +44,11 @@ export default class UpdateManager extends EventEmitter {
if (this.autoCheckData.checkEnable) {
this.autoCheckData.userCheck = false
this.options.setCheckTime.setUserConfig('last-check-update-time', new Date().getTime())
this.updater.checkForUpdates()
}
}
check () {
this.options.setCheckTime.setUserConfig('last-check-update-time', new Date().getTime())
this.autoCheckData.userCheck = true
this.updater.checkForUpdates()
}
@@ -66,8 +65,8 @@ export default class UpdateManager extends EventEmitter {
message: this.i18n.t('app.update-available-message'),
buttons: [this.i18n.t('app.yes'), this.i18n.t('app.no')],
cancelId: 1
}, (buttonIndex) => {
if (buttonIndex === 0) {
}).then(({ response }) => {
if (response === 0) {
this.updater.downloadUpdate()
}
})
@@ -102,7 +101,7 @@ export default class UpdateManager extends EventEmitter {
dialog.showMessageBox({
title: this.i18n.t('app.check-for-updates-title'),
message: this.i18n.t('app.update-downloaded-message')
}, () => {
}).then(_ => {
this.emit('will-updated')
setImmediate(() => {
this.updater.quitAndInstall()
@@ -112,7 +111,10 @@ export default class UpdateManager extends EventEmitter {
updateError (event, error) {
this.emit('update-error', error)
const msg = error == null ? this.i18n.t('update-error-message') : (error.stack || error).toString()
const msg = (error == null)
? this.i18n.t('update-error-message')
: (error.stack || error).toString()
this.updater.logger.warn(`[Motrix] update-error: ${msg}`)
dialog.showErrorBox(msg)
}
+1 -1
View File
@@ -14,7 +14,7 @@ require('electron-debug')({
})
// Install `vue-devtools`
require('electron').app.on('ready', () => {
require('electron').app.whenReady().then(() => {
let installExtension = require('electron-devtools-installer')
installExtension.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
+2 -1
View File
@@ -11,7 +11,7 @@
{ "id": "app.hide-others", "role": "hideothers" },
{ "id": "app.unhide", "role": "unhide" },
{ "type": "separator" },
{ "id": "app.quit", "role": "quit" }
{ "id": "app.quit", "command": "application:quit" }
]
},
{
@@ -29,6 +29,7 @@
{ "type": "separator" },
{ "id": "task.pause-all-task", "command": "application:pause-all-task" },
{ "id": "task.resume-all-task", "command": "application:resume-all-task" },
{ "id": "task.select-all-task", "command": "application:select-all-task" },
{ "type": "separator" },
{ "id": "task.clear-recent-tasks", "command": "application:clear-recent-tasks" }
]
+3 -2
View File
@@ -9,7 +9,7 @@
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
{ "type": "separator" },
{ "id": "app.quit", "role": "quit" }
{ "id": "app.quit", "command": "application:quit" }
]
},
{
@@ -26,7 +26,8 @@
{ "id": "task.move-task-down", "command": "application:move-task-down" },
{ "type": "separator" },
{ "id": "task.pause-all-task", "command": "application:pause-all-task" },
{ "id": "task.resume-all-task", "command": "application:resume-all-task" }
{ "id": "task.resume-all-task", "command": "application:resume-all-task" },
{ "id": "task.select-all-task", "command": "application:select-all-task" }
]
},
{
+2 -1
View File
@@ -5,7 +5,8 @@
{ "type": "separator" },
{ "id": "app.show", "command": "application:show", "command-arg": "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.quit", "role": "quit" }
{ "id": "app.quit", "command": "application:quit" }
]
+2 -1
View File
@@ -9,7 +9,7 @@
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
{ "type": "separator" },
{ "id": "app.quit", "role": "quit" }
{ "id": "app.quit", "command": "application:quit" }
]
},
{
@@ -27,6 +27,7 @@
{ "type": "separator" },
{ "id": "task.pause-all-task", "command": "application:pause-all-task" },
{ "id": "task.resume-all-task", "command": "application:resume-all-task" },
{ "id": "task.select-all-task", "command": "application:select-all-task" },
{ "type": "separator" },
{ "id": "task.clear-recent-tasks", "command": "application:clear-recent-tasks" }
]
+36
View File
@@ -0,0 +1,36 @@
import is from 'electron-is'
import { EventEmitter } from 'events'
import { app } from 'electron'
import {
APP_RUN_MODE
} from '@shared/constants'
const isMac = is.macOS()
export default class DockManager extends EventEmitter {
constructor (options) {
super()
this.options = options
const { runMode } = this.options
if (runMode !== APP_RUN_MODE.STANDARD) {
this.hide()
}
}
show = isMac ? () => {
return app.dock.show()
} : () => {}
hide = isMac ? () => {
app.dock.hide()
} : () => {}
setBadge = isMac ? (text) => {
app.dock.setBadge(text)
} : (text) => {}
openDock = isMac ? (path) => {
app.dock.downloadFinished(path)
} : (path) => {}
}
+7 -6
View File
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events'
import { Menu } from 'electron'
import {
translateTemplate,
flattenMenuItems,
@@ -23,13 +24,13 @@ export default class MenuManager extends EventEmitter {
}
load () {
let template = require(`../menus/${process.platform}.json`)
this.template = template['menu']
const template = require(`../menus/${process.platform}.json`)
this.template = template.menu
}
build () {
const keystrokesByCommand = {}
for (let item in this.keymap) {
for (const item in this.keymap) {
keystrokesByCommand[this.keymap[item]] = item
}
@@ -50,7 +51,7 @@ export default class MenuManager extends EventEmitter {
this.setup()
}
updateStates (visibleStates, enabledStates, checkedStates) {
updateMenuStates (visibleStates, enabledStates, checkedStates) {
updateStates(this.items, visibleStates, enabledStates, checkedStates)
}
@@ -58,13 +59,13 @@ export default class MenuManager extends EventEmitter {
const visibleStates = {
[id]: flag
}
this.updateStates(visibleStates, null, null)
this.updateMenuStates(visibleStates, null, null)
}
updateMenuItemEnabledState (id, flag) {
const enabledStates = {
[id]: flag
}
this.updateStates(null, enabledStates, null)
this.updateMenuStates(null, enabledStates, null)
}
}
+11 -13
View File
@@ -1,7 +1,8 @@
import { EventEmitter } from 'events'
import { systemPreferences } from 'electron'
import { nativeTheme, systemPreferences } from 'electron'
import is from 'electron-is'
import { LIGHT_THEME, DARK_THEME } from '@shared/constants'
import { APP_THEME } from '@shared/constants'
export default class ThemeManager extends EventEmitter {
constructor (options = {}) {
@@ -15,11 +16,11 @@ export default class ThemeManager extends EventEmitter {
}
getSystemTheme () {
let result = LIGHT_THEME
let result = APP_THEME.LIGHT
if (!is.macOS()) {
return result
}
result = systemPreferences.isDarkMode() ? DARK_THEME : LIGHT_THEME
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
@@ -27,18 +28,15 @@ export default class ThemeManager extends EventEmitter {
if (!is.macOS()) {
return
}
systemPreferences.subscribeNotification(
'AppleInterfaceThemeChangedNotification',
() => {
const theme = this.getSystemTheme()
this.updateAppAppearance(theme)
this.emit('system-theme-changed', theme)
}
)
nativeTheme.on('updated', () => {
const theme = this.getSystemTheme()
this.updateAppAppearance(theme)
this.emit('system-theme-changed', theme)
})
}
updateAppAppearance (theme) {
if (!is.macOS() || theme !== LIGHT_THEME || theme !== DARK_THEME) {
if (!is.macOS() || theme !== APP_THEME.LIGHT || theme !== APP_THEME.DARK) {
return
}
systemPreferences.setAppLevelAppearance(theme)
+28 -25
View File
@@ -1,6 +1,7 @@
import { EventEmitter } from 'events'
import { join } from 'path'
import { TouchBar, nativeImage } from 'electron'
import { handleCommand } from '../utils/menu'
import logger from '../core/Logger'
@@ -15,7 +16,7 @@ export default class TouchBarManager extends EventEmitter {
}
load () {
this.template = require(`../menus/touchBar.json`)
this.template = require('../menus/touchBar.json')
}
getClickFn (item) {
@@ -41,30 +42,32 @@ export default class TouchBarManager extends EventEmitter {
const { label, backgroundColor, textColor, size } = options
switch (type) {
case 'button':
const icon = this.getIconImage(options.icon)
const click = this.getClickFn(options)
result = new TouchBarButton({
label,
backgroundColor,
icon,
click
case 'button':
result = new TouchBarButton({
label,
backgroundColor,
icon: this.getIconImage(options.icon),
click: this.getClickFn(options)
})
break
case 'label':
result = new TouchBarLabel({
label,
textColor
})
break
case 'spacer':
result = new TouchBarSpacer({ size })
break
case 'group':
result = new TouchBarGroup({
items: new TouchBar({
items: options.items
})
break
case 'label':
result = new TouchBarLabel({
label,
textColor
})
break
case 'spacer':
result = new TouchBarSpacer({ size })
break
case 'group':
result = new TouchBarGroup({ items: options.items })
break
default:
result = null
})
break
default:
result = null
}
return result
@@ -90,7 +93,7 @@ export default class TouchBarManager extends EventEmitter {
if (!bar) {
try {
const items = this.build(this.template)
bar = new TouchBar(items)
bar = new TouchBar({ items })
this.bars[page] = bar
} catch (e) {
logger.info('getTouchBarByPage fail', e)
+51 -20
View File
@@ -1,10 +1,15 @@
import { EventEmitter } from 'events'
import { join } from 'path'
import { Tray, Menu, systemPreferences } from 'electron'
import { Tray, Menu, nativeTheme } from 'electron'
import is from 'electron-is'
import { translateTemplate } from '../utils/menu'
import {
translateTemplate,
flattenMenuItems,
updateStates
} from '../utils/menu'
import { getI18n } from '@/ui/Locale'
import { LIGHT_THEME, DARK_THEME } from '@shared/constants'
import { APP_THEME } from '@shared/constants'
let tray = null
@@ -12,8 +17,8 @@ export default class TrayManager extends EventEmitter {
constructor (options = {}) {
super()
this.theme = options.theme || APP_THEME.AUTO
this.i18n = getI18n()
this.menu = null
this.load()
@@ -23,21 +28,29 @@ export default class TrayManager extends EventEmitter {
}
load () {
this.template = require(`../menus/tray.json`)
const theme = systemPreferences.isDarkMode() ? DARK_THEME : LIGHT_THEME
this.template = require('../menus/tray.json')
if (is.macOS()) {
this.normalIcon = join(__static, `./mo-tray-${theme}-normal.png`)
this.activeIcon = join(__static, `./mo-tray-${theme}-active.png`)
} else {
this.normalIcon = join(__static, './mo-tray-colorful-normal.png')
this.activeIcon = join(__static, './mo-tray-colorful-active.png')
let theme = APP_THEME.LIGHT
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
}
this.setIcons(theme)
}
setIcons (theme) {
this.normalIcon = join(__static, `./mo-tray-${theme}-normal.png`)
this.activeIcon = join(__static, `./mo-tray-${theme}-active.png`)
}
build () {
const keystrokesByCommand = {}
for (let item in this.keymap) {
for (const item in this.keymap) {
keystrokesByCommand[this.keymap[item]] = item
}
@@ -45,6 +58,7 @@ export default class TrayManager extends EventEmitter {
const template = JSON.parse(JSON.stringify(this.template))
const tpl = translateTemplate(template, keystrokesByCommand, this.i18n)
this.menu = Menu.buildFromTemplate(tpl)
this.items = flattenMenuItems(this.menu)
}
setup () {
@@ -92,25 +106,42 @@ export default class TrayManager extends EventEmitter {
global.application.handleFile(files[0])
}
updateStatus (status) {
updateTrayByStatus (status) {
this.status = status
this.updateIcon()
this.updateTray()
}
updateIcon () {
updateTray () {
const icon = this.status ? this.activeIcon : this.normalIcon
tray.setImage(icon)
}
changeIconTheme (theme = LIGHT_THEME) {
changeIconTheme (theme = APP_THEME.LIGHT) {
if (!is.macOS()) {
return
}
this.normalIcon = join(__static, `./mo-tray-${theme}-normal.png`)
this.activeIcon = join(__static, `./mo-tray-${theme}-active.png`)
this.setIcons(theme)
this.updateIcon()
this.updateTray()
}
updateMenuStates (visibleStates, enabledStates, checkedStates) {
updateStates(this.items, visibleStates, enabledStates, checkedStates)
}
updateMenuItemVisibleState (id, flag) {
const visibleStates = {
[id]: flag
}
this.updateMenuStates(visibleStates, null, null)
}
updateMenuItemEnabledState (id, flag) {
const enabledStates = {
[id]: flag
}
this.updateMenuStates(null, enabledStates, null)
}
destroy () {
+68 -16
View File
@@ -4,15 +4,16 @@ import { app, shell, screen, BrowserWindow } from 'electron'
import is from 'electron-is'
import pageConfig from '../configs/page'
import logger from '../core/Logger'
import { debounce } from 'lodash'
const defaultBrowserOptions = {
titleBarStyle: 'hiddenInset',
useContentSize: true,
show: false,
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true
nodeIntegration: true,
webviewTag: true
}
}
@@ -57,41 +58,63 @@ export default class WindowManager extends EventEmitter {
return result
}
openWindow (page) {
const options = this.getPageOptions(page)
getPageBounds (page) {
const enabled = this.userConfig['keep-window-state']
const windowStateMap = this.userConfig['window-state'] || {}
let result = null
if (enabled) {
result = windowStateMap[page]
}
return result
}
openWindow (page, options = {}) {
const pageOptions = this.getPageOptions(page)
const { hidden } = options
const autoHideWindow = this.userConfig['auto-hide-window']
let window = this.windows[page] || null
if (window) {
window.restore()
window.show()
window.focus()
return window
}
window = new BrowserWindow({
...defaultBrowserOptions,
...options.attrs
...pageOptions.attrs
})
const bounds = this.getPageBounds(page)
if (bounds) {
window.setBounds(bounds)
}
window.webContents.on('new-window', (e, url) => {
e.preventDefault()
shell.openExternal(url)
})
if (options.url) {
window.loadURL(options.url)
if (pageOptions.url) {
window.loadURL(pageOptions.url)
}
window.once('ready-to-show', () => {
window.show()
if (!hidden) {
window.show()
}
})
if (options.bindCloseToHide) {
this.bindCloseToHide(page, window)
}
this.handleWindowState(page, window)
this.handleWindowClose(pageOptions, page, window)
this.bindAfterClosed(page, window)
this.addWindow(page, window)
if (autoHideWindow) {
this.handleWindowBlur()
}
return window
}
@@ -114,6 +137,9 @@ export default class WindowManager extends EventEmitter {
destroyWindow (page) {
const win = this.getWindow(page)
this.removeWindow(page)
win.removeListener('closed')
win.removeListener('move')
win.removeListener('resize')
win.destroy()
}
@@ -127,12 +153,26 @@ export default class WindowManager extends EventEmitter {
})
}
bindCloseToHide (page, window) {
handleWindowState (page, window) {
window.on('resize', debounce(() => {
const bounds = window.getBounds()
this.emit('window-resized', { page, bounds })
}, 500))
window.on('move', debounce(() => {
const bounds = window.getBounds()
this.emit('window-moved', { page, bounds })
}, 500))
}
handleWindowClose (pageOptions, page, window) {
window.on('close', (event) => {
if (!this.willQuit) {
if (pageOptions.bindCloseToHide && !this.willQuit) {
event.preventDefault()
window.hide()
}
const bounds = window.getBounds()
this.emit('window-closed', { page, bounds })
})
}
@@ -144,7 +184,7 @@ export default class WindowManager extends EventEmitter {
window.show()
}
hideWindow (page) {
autoHideWindow (page) {
const window = this.getWindow(page)
if (!window) {
return
@@ -180,6 +220,18 @@ export default class WindowManager extends EventEmitter {
})
}
onWindowBlur (event, window) {
window.hide()
}
handleWindowBlur () {
app.on('browser-window-blur', this.onWindowBlur)
}
unbindWindowBlur () {
app.removeListener('browser-window-blur', this.onWindowBlur)
}
handleAllWindowClosed () {
app.on('window-all-closed', (event) => {
event.preventDefault()
@@ -190,7 +242,7 @@ export default class WindowManager extends EventEmitter {
if (!window) {
return
}
logger.info('[Motrix] sendCommandTo===>', window, command, ...args)
logger.info('[Motrix] send command to:', command, ...args)
window.webContents.send('command', command, ...args)
}
+47 -13
View File
@@ -2,6 +2,11 @@ import { app } from 'electron'
import is from 'electron-is'
import { resolve } from 'path'
import { existsSync, lstatSync } from 'fs'
import {
ENGINE_MAX_CONNECTION_PER_SERVER,
IP_VERSION
} from '@shared/constants'
import logger from '../core/Logger'
import engineBinMap from '../configs/engine'
@@ -10,7 +15,7 @@ export function getLogPath () {
}
export function getDhtPath (protocol) {
const name = protocol === 6 ? 'dht6.dat' : 'dht.dat'
const name = protocol === IP_VERSION.V6 ? 'dht6.dat' : 'dht.dat'
return resolve(app.getPath('userData'), `./${name}`)
}
@@ -27,12 +32,12 @@ export function getUserDownloadsPath () {
}
export function getEngineBin (platform) {
let result = engineBinMap.hasOwnProperty(platform) ? engineBinMap[platform] : ''
const result = engineBinMap[platform] || ''
return result
}
export function transformConfig (config) {
let result = []
const result = []
for (const [k, v] of Object.entries(config)) {
if (v !== '') {
result.push(`--${k}=${v}`)
@@ -65,25 +70,50 @@ export function moveAppToApplicationsFolder (errorMsg = '') {
})
}
export function splitArgv (argv) {
const args = []
const extra = {}
for (const arg of argv) {
if (arg.startsWith('--')) {
const kv = arg.split('=')
const key = kv[0]
const value = kv[1] || '1'
extra[key] = value
continue
}
args.push(arg)
}
return { args, extra }
}
export function parseArgvAsUrl (argv) {
let arg = argv[1]
const arg = argv[1]
if (!arg) {
return
}
if (
arg.toLowerCase().startsWith('mo:') ||
arg.toLowerCase().startsWith('motrix:') ||
arg.toLowerCase().startsWith('http:') ||
arg.toLowerCase().startsWith('https:') ||
arg.toLowerCase().startsWith('ftp:') ||
arg.toLowerCase().startsWith('magnet:') ||
arg.toLowerCase().startsWith('thunder:')
) {
if (checkIsSupportedSchema(arg)) {
return arg
}
}
export function checkIsSupportedSchema (url = '') {
const str = url.toLowerCase()
if (
str.startsWith('mo:') ||
str.startsWith('motrix:') ||
str.startsWith('http:') ||
str.startsWith('https:') ||
str.startsWith('ftp:') ||
str.startsWith('magnet:') ||
str.startsWith('thunder:')
) {
return true
} else {
return false
}
}
export function isDirectory (path) {
return existsSync(path) && lstatSync(path).isDirectory()
}
@@ -99,3 +129,7 @@ export function parseArgvAsFile (argv) {
}
return arg
}
export const getMaxConnectionPerServer = () => {
return ENGINE_MAX_CONNECTION_PER_SERVER
}
+42 -46
View File
@@ -3,31 +3,31 @@ export function concat (template, submenu, submenuToAdd) {
let relativeItem = null
if (sub.position) {
switch (sub.position) {
case 'first':
submenu.unshift(sub)
break
case 'last':
submenu.push(sub)
break
case 'before':
relativeItem = findById(template, sub['relative-id'])
if (relativeItem) {
let array = relativeItem.__parent
let index = array.indexOf(relativeItem)
array.splice(index, 0, sub)
}
break
case 'after':
relativeItem = findById(template, sub['relative-id'])
if (relativeItem) {
let array = relativeItem.__parent
let index = array.indexOf(relativeItem)
array.splice(index + 1, 0, sub)
}
break
default:
submenu.push(sub)
break
case 'first':
submenu.unshift(sub)
break
case 'last':
submenu.push(sub)
break
case 'before':
relativeItem = findById(template, sub['relative-id'])
if (relativeItem) {
const array = relativeItem.__parent
const index = array.indexOf(relativeItem)
array.splice(index, 0, sub)
}
break
case 'after':
relativeItem = findById(template, sub['relative-id'])
if (relativeItem) {
const array = relativeItem.__parent
const index = array.indexOf(relativeItem)
array.splice(index + 1, 0, sub)
}
break
default:
submenu.push(sub)
break
}
} else {
submenu.push(sub)
@@ -37,7 +37,7 @@ export function concat (template, submenu, submenuToAdd) {
export function merge (template, item) {
if (item.id) {
let matched = findById(template, item.id)
const matched = findById(template, item.id)
if (matched) {
if (item.submenu && Array.isArray(item.submenu)) {
if (!Array.isArray(matched.submenu)) {
@@ -54,15 +54,15 @@ export function merge (template, item) {
}
function findById (template, id) {
for (let i in template) {
let item = template[i]
for (const i in template) {
const item = template[i]
if (item.id === id) {
// Returned item need to have a reference to parent Array (.__parent).
// This is required to handle `position` and `relative-id`
item.__parent = template
return item
} else if (Array.isArray(item.submenu)) {
let result = findById(item.submenu, id)
const result = findById(item.submenu, id)
if (result) {
return result
}
@@ -72,8 +72,8 @@ function findById (template, id) {
}
export function translateTemplate (template, keystrokesByCommand, i18n) {
for (let i in template) {
let item = template[i]
for (const i in template) {
const item = template[i]
if (item.command) {
item.accelerator = acceleratorForCommand(item.command, keystrokesByCommand)
}
@@ -112,22 +112,18 @@ export function handleCommand (item) {
}
function handleCommandBefore (item) {
console.log('handleCommandBefore==1=>', item)
if (!item['command-before']) {
return
}
const [ command, ...args ] = item['command-before'].split(',')
console.log('handleCommandBefore==2=>', command, ...args)
const [command, ...args] = item['command-before'].split(',')
global.application.sendCommandToAll(command, ...args)
}
function handleCommandAfter (item) {
console.log('handleCommandAfter==1=>', item)
if (!item['command-after']) {
return
}
const [ command, ...args ] = item['command-after'].split(',')
console.log('handleCommandAfter==2=>', command, ...args)
const [command, ...args] = item['command-after'].split(',')
global.application.sendCommandToAll(command, ...args)
}
@@ -135,7 +131,7 @@ function acceleratorForCommand (command, keystrokesByCommand) {
const keystroke = keystrokesByCommand[command]
if (keystroke) {
let modifiers = keystroke.split(/-(?=.)/)
let key = modifiers.pop().toUpperCase()
const key = modifiers.pop().toUpperCase()
.replace('+', 'Plus')
.replace('MINUS', '-')
modifiers = modifiers.map((modifier) => {
@@ -152,14 +148,14 @@ function acceleratorForCommand (command, keystrokesByCommand) {
.replace(/alt/ig, 'Alt')
}
})
let keys = modifiers.concat([key])
const keys = modifiers.concat([key])
return keys.join('+')
}
return null
}
export function flattenMenuItems (menu) {
let flattenItems = {}
const flattenItems = {}
menu.items.forEach(item => {
if (item.id) {
flattenItems[item.id] = item
@@ -173,24 +169,24 @@ export function flattenMenuItems (menu) {
export function updateStates (itemsById, visibleStates, enabledStates, checkedStates) {
if (visibleStates) {
for (let command in visibleStates) {
let item = itemsById[command]
for (const command in visibleStates) {
const item = itemsById[command]
if (item) {
item.visible = visibleStates[command]
}
}
}
if (enabledStates) {
for (let command in enabledStates) {
let item = itemsById[command]
for (const command in enabledStates) {
const item = itemsById[command]
if (item) {
item.enabled = enabledStates[command]
}
}
}
if (checkedStates) {
for (let id in checkedStates) {
let item = itemsById[id]
for (const id in checkedStates) {
const item = itemsById[id]
if (item) {
item.checked = checkedStates[id]
}
+116 -47
View File
@@ -1,18 +1,16 @@
import { remote } from 'electron'
import { ipcRenderer, remote } from 'electron'
import is from 'electron-is'
import { isEmpty } from 'lodash'
import Aria2 from 'aria2'
import {
separateConfig,
compactUndefined,
formatOptionsForEngine,
mergeTaskResult,
changeKeysToCamelCase,
changeKeysToKebabCase
} from '@shared/utils'
import {
BEST_TRACKERS_URL,
BEST_TRACKERS_IP_URL
} from '@shared/constants'
import { ENGINE_RPC_HOST } from '@shared/constants'
const application = remote.getGlobal('application')
@@ -57,7 +55,9 @@ export default class Api {
rpcListenPort: port,
rpcSecret: secret
} = this.config
const host = ENGINE_RPC_HOST
this.client = new Aria2({
host,
port,
secret
})
@@ -84,9 +84,9 @@ export default class Api {
savePreference (params = {}) {
const kebabParams = changeKeysToKebabCase(params)
if (is.renderer()) {
this.savePreferenceToNativeStore(kebabParams)
return this.savePreferenceToNativeStore(kebabParams)
} else {
this.savePreferenceToLocalStorage(kebabParams)
return this.savePreferenceToLocalStorage(kebabParams)
}
}
@@ -96,25 +96,36 @@ export default class Api {
savePreferenceToNativeStore (params = {}) {
const { user, system, others } = separateConfig(params)
if (!isEmpty(system)) {
console.info('[Motrix] save system config: ', system)
application.configManager.setSystemConfig(system)
}
const config = {}
if (!isEmpty(user)) {
console.info('[Motrix] save user config: ', user)
application.configManager.setUserConfig(user)
config.user = user
}
if (!isEmpty(system)) {
console.info('[Motrix] save system config: ', system)
config.system = system
this.updateActiveTaskOption(system)
}
if (!isEmpty(others)) {
console.info('[Motrix] save config found iillegal key: ', others)
console.info('[Motrix] save config found illegal key: ', others)
}
ipcRenderer.send('command', 'application:save-preference', config)
}
getVersion () {
return this.client.call('getVersion')
}
changeGlobalOption (options) {
const args = formatOptionsForEngine(options)
return this.client.call('changeGlobalOption', args)
}
getGlobalOption () {
return new Promise((resolve) => {
this.client.call('getGlobalOption')
@@ -124,6 +135,40 @@ export default class Api {
})
}
getOption (params = {}) {
const { gid } = params
const args = compactUndefined([gid])
return new Promise((resolve) => {
this.client.call('getOption', ...args)
.then((data) => {
resolve(changeKeysToCamelCase(data))
})
})
}
updateActiveTaskOption (options) {
this.fetchTaskList({ type: 'active' })
.then((data) => {
if (isEmpty(data)) {
return
}
const gids = data.map((task) => task.gid)
this.batchChangeOption({ gids, options })
})
}
changeOption (params = {}) {
let { gid, options = {} } = params
options = formatOptionsForEngine(options)
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([gid, kebabOptions])
return this.client.call('changeOption', ...args)
}
getGlobalStat () {
return this.client.call('getGlobalStat')
}
@@ -131,11 +176,16 @@ export default class Api {
addUri (params) {
const {
uris,
outs,
options
} = params
const tasks = uris.map((uri) => {
const args = compactUndefined([[uri], options])
return [ 'aria2.addUri', ...args ]
const tasks = uris.map((uri, index) => {
const kebabOptions = changeKeysToKebabCase(options)
if (outs && outs[index]) {
kebabOptions.out = outs[index]
}
const args = compactUndefined([[uri], kebabOptions])
return ['aria2.addUri', ...args]
})
return this.client.multicall(tasks)
}
@@ -145,7 +195,8 @@ export default class Api {
torrent,
options
} = params
const args = compactUndefined([torrent, [], options])
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([torrent, [], kebabOptions])
return this.client.call('addTorrent', ...args)
}
@@ -154,7 +205,8 @@ export default class Api {
metalink,
options
} = params
const args = compactUndefined([metalink, options])
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([metalink, kebabOptions])
return this.client.call('addMetalink', ...args)
}
@@ -164,14 +216,14 @@ export default class Api {
const waitingArgs = compactUndefined([offset, num, keys])
return new Promise((resolve, reject) => {
this.client.multicall([
[ 'aria2.tellActive', ...activeArgs ],
[ 'aria2.tellWaiting', ...waitingArgs ]
['aria2.tellActive', ...activeArgs],
['aria2.tellWaiting', ...waitingArgs]
]).then((data) => {
console.log('fetchDownloadingTaskList data', data)
console.log('[Motrix] fetch downloading task list data:', data)
const result = mergeTaskResult(data)
resolve(result)
}).catch((err) => {
console.log('fetchDownloadingTaskList fail===>', err)
console.log('[Motrix] fetch downloading task list fail:', err)
reject(err)
})
})
@@ -192,14 +244,14 @@ export default class Api {
fetchTaskList (params = {}) {
const { type } = params
switch (type) {
case 'active':
return this.fetchDownloadingTaskList(params)
case 'waiting':
return this.fetchWaitingTaskList(params)
case 'stopped':
return this.fetchStoppedTaskList(params)
default:
return this.fetchDownloadingTaskList(params)
case 'active':
return this.fetchDownloadingTaskList(params)
case 'waiting':
return this.fetchWaitingTaskList(params)
case 'stopped':
return this.fetchStoppedTaskList(params)
default:
return this.fetchDownloadingTaskList(params)
}
}
@@ -209,6 +261,12 @@ export default class Api {
return this.client.call('tellStatus', ...args)
}
fetchTaskItemPeers (params = {}) {
const { gid, keys } = params
const args = compactUndefined([gid, keys])
return this.client.call('getPeers', ...args)
}
pauseTask (params = {}) {
const { gid } = params
const args = compactUndefined([gid])
@@ -265,24 +323,35 @@ export default class Api {
return this.client.call('removeDownloadResult', ...args)
}
startPowerSaveBlocker () {
application.energyManager.startPowerSaveBlocker()
}
multicall (method, params = {}) {
let { gids, options = {} } = params
options = formatOptionsForEngine(options)
stopPowerSaveBlocker () {
application.energyManager.stopPowerSaveBlocker()
}
fetchBtTrackerFromGitHub () {
const now = Date.now()
const promises = [
fetch(`${BEST_TRACKERS_IP_URL}?t=${now}`).then((res) => res.text()),
fetch(`${BEST_TRACKERS_URL}?t=${now}`).then((res) => res.text())
]
return Promise.all(promises).then((values) => {
let result = values.join('\r\n').replace(/^\s*[\r\n]/gm, '')
return result
const data = gids.map((gid, index) => {
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([gid, kebabOptions])
return [method, ...args]
})
return this.client.multicall(data)
}
batchChangeOption (params = {}) {
return this.multicall('aria2.changeOption', params)
}
batchRemoveTask (params = {}) {
return this.multicall('aria2.remove', params)
}
batchResumeTask (params = {}) {
return this.multicall('aria2.unpause', params)
}
batchPauseTask (params = {}) {
return this.multicall('aria2.pause', params)
}
batchForcePauseTask (params = {}) {
return this.multicall('aria2.forcePause', params)
}
}
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<line data-cap="butt" data-color="color-2" fill="none" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="19,15 12,22 5,15 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 429 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<line data-cap="butt" data-color="color-2" fill="none" stroke-miterlimit="10" x1="12" y1="22" x2="12" y2="2"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="5,9 12,2 19,9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<rect x="4" y="3" width="16" height="18" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<line data-color="color-2" x1="1" y1="6" x2="1" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-color="color-2" x1="23" y1="6" x2="23" y2="18" fill="none" stroke-miterlimit="10"/>
<polyline data-cap="butt" points="10 15 10 8 16 8 16 14" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<circle data-stroke="none" cx="9" cy="15" r="2" stroke="none"/>
<circle data-stroke="none" cx="15" cy="14" r="2" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<polyline data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" points="17,23 17,7 1,7 "/>
<line data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" x1="17" y1="7" x2="23" y2="1"/>
<polygon fill="none" stroke="currentColor" stroke-miterlimit="10" points="17,23 23,17 23,1 7,1 1,7 1,23 "/>
<circle data-color="color-2" data-stroke="none" cx="12" cy="12" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="6" cy="12" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="12" cy="18" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="6" cy="18" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<polyline data-cap="butt" data-color="color-2" points="1 20 6 14 10 18 17 10 23 17" fill="none" stroke-miterlimit="10"/>
<rect x="1" y="3" width="22" height="18" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<circle data-color="color-2" cx="9" cy="8" r="2" fill="none" stroke-miterlimit="10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 509 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<polyline data-cap="butt" fill="none" stroke-miterlimit="10" points="12,9 12,13 7.8,16.1 "/>
<line data-cap="butt" fill="none" stroke-miterlimit="10" x1="12" y1="13" x2="16.2" y2="16.1"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="12" cy="5" r="4"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="5" cy="19" r="4"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="19" cy="19" r="4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 687 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g>
<path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12s12-5.383,12-12S18.617,0,12,0z M19,13h-8V5h2v6h6V13z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 214 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<path data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" d="M6.121,20.121 C7.727,21.22,9.907,22,12,22c5.523,0,10-4.477,10-10c0-5.523-4.477-10-10-10C8.101,2,4.728,4.233,3.078,7.488"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="2.278,1.588 3.078,7.488 9.078,6.688 "/>
<circle data-color="color-2" fill="none" stroke-miterlimit="10" cx="4" cy="18" r="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 609 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<line data-cap="butt" data-color="color-2" x1="2" y1="6" x2="22" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="12" y1="2" x2="12" y2="22" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="7" y1="2" x2="7" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="17" y1="2" x2="17" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="2" y1="18" x2="22" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="7" y1="22" x2="7" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="17" y1="22" x2="17" y2="18" fill="none" stroke-miterlimit="10"/>
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -3
View File
@@ -12,7 +12,6 @@
</template>
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import AppInfo from '@/components/About/AppInfo'
import Copyright from '@/components/About/Copyright'
@@ -41,8 +40,6 @@
})
},
methods: {
isRenderer: is.renderer,
isMas: is.mas,
handleOpen () {
this.$store.dispatch('app/fetchEngineInfo')
},
@@ -58,6 +55,7 @@
<style lang="scss">
.app-about-dialog {
max-width: 632px;
min-width: 380px;
.el-dialog__header {
padding-top: 0;
padding-bottom: 0;
+6 -4
View File
@@ -34,9 +34,11 @@
},
engine: {
type: Object,
default: {
version: '',
enabledFeatures: []
default () {
return {
version: '',
enabledFeatures: []
}
}
}
},
@@ -71,7 +73,7 @@
margin: 50px 35% 0 8px;
h4 {
font-size: $--font-size-base;
font-weight: $--font-weight-secondary;
font-weight: normal;
color: $--app-engine-title-color;
}
ul {
+8 -5
View File
@@ -1,18 +1,21 @@
<template>
<el-row class="copyright">
<el-col :span="6" class="copyright-left">
<a target="_blank" href="https://motrix.app/" rel="noopener noreferrer">
&copy;2018 Motrix
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/">
&copy;2019 Motrix
</a>
</el-col>
<el-col :span="18" class="copyright-right">
<a target="_blank" href="https://motrix.app/about" rel="noopener noreferrer">
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/license">
{{ $t('about.license') }}
</a>
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/about">
{{ $t('about.about') }}
</a>
<a target="_blank" href="https://motrix.app/support" rel="noopener noreferrer">
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/support">
{{ $t('about.support') }}
</a>
<a target="_blank" href="https://motrix.app/release" rel="noopener noreferrer">
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/release">
{{ $t('about.release') }}
</a>
</el-col>
+9 -13
View File
@@ -1,20 +1,20 @@
<template>
<el-aside width="78px" :class="['aside', { 'draggable': asideDraggable }]">
<el-aside width="78px" :class="['aside', 'hidden-sm-and-down', { 'draggable': asideDraggable }]">
<div class="aside-inner">
<mo-logo-mini />
<ul class="menu top-menu">
<li @click="nav('/task')">
<li @click="nav('/task')" class="non-draggable">
<mo-icon name="menu-task" width="20" height="20" />
</li>
<li @click="showAddTask()">
<li @click="showAddTask()" class="non-draggable">
<mo-icon name="menu-add" width="20" height="20" />
</li>
</ul>
<ul class="menu bottom-menu">
<li @click="nav('/preference')">
<li @click="nav('/preference')" class="non-draggable">
<mo-icon name="menu-preference" width="20" height="20" />
</li>
<li @click="showAboutPanel">
<li @click="showAboutPanel" class="non-draggable">
<mo-icon name="menu-about" width="20" height="20" />
</li>
</ul>
@@ -25,6 +25,7 @@
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import { ADD_TASK_TYPE } from '@shared/constants'
import LogoMini from '@/components/Logo/LogoMini'
import '@/components/Icons/menu-task'
import '@/components/Icons/menu-add'
@@ -45,22 +46,17 @@
}
},
methods: {
open (link) {
this.$electron.shell.openExternal(link)
},
showAddTask (taskType = 'uri') {
showAddTask (taskType = ADD_TASK_TYPE.URI) {
this.$store.dispatch('app/showAddTaskDialog', taskType)
},
showAboutPanel () {
// if (is.renderer()) {
// this.$electron.ipcRenderer.send('command', 'application:about')
// } else {
this.$store.dispatch('app/showAboutPanel')
// }
},
nav (page) {
this.$router.push({
path: page
}).catch(err => {
console.log(err)
})
}
}
+78
View File
@@ -0,0 +1,78 @@
<template>
<div ref="webviewViewport" class="webview-viewport">
<webview
class="mo-webview"
ref="webview"
:src="src"
></webview>
</div>
</template>
<script>
import is from 'electron-is'
import { Loading } from 'element-ui'
import {
openExternal
} from '@/components/Native/utils'
export default {
name: 'mo-browser',
components: {
},
props: {
src: {
type: String,
default: ''
}
},
data () {
return {
loading: null
}
},
computed: {
isRenderer: () => is.renderer()
},
mounted () {
const { webview } = 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))
},
methods: {
loadStart () {
const { webviewViewport } = this.$refs
this.loading = Loading.service({
target: webviewViewport
})
},
loadStop () {
this.$nextTick(() => {
this.loading.close()
})
},
ready () {
const { webview } = this.$refs
const webContents = this.$electron.remote.webContents.fromId(webview.getWebContentsId())
webContents.on('new-window', (event, url) => {
event.preventDefault()
openExternal(url)
})
}
}
}
</script>
<style lang="scss">
.webview-viewport {
position: relative;
}
.mo-webview {
display: inline-flex;;
flex: 1;
flex-basis: auto;
}
</style>
+23 -12
View File
@@ -1,10 +1,12 @@
import { Message } from 'element-ui'
import { base64StringToBlob } from 'blob-util'
import router from '@/router'
import store from '@/store'
import CommandManager from './CommandManager'
import { Message } from 'element-ui'
import { getLocaleManager } from '@/components/Locale'
import { base64StringToBlob } from 'blob-util'
import { buildFileList } from '@shared/utils'
import { ADD_TASK_TYPE } from '@shared/constants'
import { getLocaleManager } from '@/components/Locale'
import CommandManager from './CommandManager'
const commands = new CommandManager()
const i18n = getLocaleManager().getI18n()
@@ -21,33 +23,37 @@ function showAboutPanel () {
store.dispatch('app/showAboutPanel')
}
function showAddTask (taskType = 'uri', task = '') {
if (taskType === 'uri' && task) {
function showAddTask (taskType = ADD_TASK_TYPE.URI, task = '') {
if (taskType === ADD_TASK_TYPE.URI && task) {
store.dispatch('app/updateAddTaskUrl', task)
}
store.dispatch('app/showAddTaskDialog', taskType)
}
function showAddBtTask () {
store.dispatch('app/showAddTaskDialog', 'torrent')
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
}
function showAddBtTaskWithFile (fileName, base64Data = '') {
const blob = base64StringToBlob(base64Data, 'application/x-bittorrent')
const file = new File([blob], fileName, { type: 'application/x-bittorrent' })
const fileList = buildFileList(file)
store.dispatch('app/showAddTaskDialog', 'torrent')
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
setTimeout(() => {
store.dispatch('app/addTaskAddTorrents', { fileList })
}, 200)
}
function navigateTaskList (status = 'active') {
router.push({ path: `/task/${status}` })
router.push({ path: `/task/${status}` }).catch(err => {
console.log(err)
})
}
function navigatePreferences () {
router.push({ path: '/preference' })
router.push({ path: '/preference' }).catch(err => {
console.log(err)
})
}
function showUnderDevelopmentMessage () {
@@ -55,11 +61,11 @@ function showUnderDevelopmentMessage () {
}
function pauseTask () {
showUnderDevelopmentMessage()
store.dispatch('task/batchPauseSelectedTasks')
}
function resumeTask () {
showUnderDevelopmentMessage()
store.dispatch('task/batchResumeSelectedTasks')
}
function deleteTask () {
@@ -82,6 +88,10 @@ function resumeAllTask () {
store.dispatch('task/resumeAllTask')
}
function selectAllTask () {
store.dispatch('task/selectAllTask')
}
commands.register('application:system-theme', updateSystemTheme)
commands.register('application:theme', updateTheme)
commands.register('application:about', showAboutPanel)
@@ -98,6 +108,7 @@ commands.register('application:move-task-up', moveTaskUp)
commands.register('application:move-task-down', moveTaskDown)
commands.register('application:pause-all-task', pauseAllTask)
commands.register('application:resume-all-task', resumeAllTask)
commands.register('application:select-all-task', selectAllTask)
export {
commands
@@ -0,0 +1,162 @@
<template>
<div
ref="container"
style="position: relative; user-select: none; overflow-x: hidden; touch-action: none;"
>
<slot v-bind="{ selected: intersected }" />
</div>
</template>
<script>
const getDimensions = (p1, p2) => ({
width: Math.abs(p1.x - p2.x),
height: Math.abs(p1.y - p2.y)
})
const collisionCheck = (node1, node2) =>
node1.left < node2.left + node2.width &&
node1.left + node1.width > node2.left &&
node1.top < node2.top + node2.height &&
node1.top + node1.height > node2.top
export default {
name: 'mo-drag-select',
props: {
attribute: {
type: String,
required: true
},
color: {
type: String,
default: '#bad7fb'
},
opacity: {
type: Number,
default: 0.7
}
},
data () {
return {
intersected: [],
children: []
}
},
watch: {
intersected (i) {
this.$emit('change', i)
}
},
mounted () {
const { container } = this.$refs
const self = this
let containerRect = container.getBoundingClientRect()
const getCoords = e => ({
x: e.clientX - containerRect.left,
y: e.clientY - containerRect.top
})
const box = this.createBox()
let start = { x: 0, y: 0 }
let end = { x: 0, y: 0 }
function touchStart (e) {
e.preventDefault()
startDrag(e.touches[0])
}
function touchMove (e) {
e.preventDefault()
drag(e.touches[0])
}
function startDrag (e) {
containerRect = container.getBoundingClientRect()
self.children = container.childNodes
start = getCoords(e)
end = start
document.addEventListener('mousemove', drag)
document.addEventListener('touchmove', touchMove)
box.style.top = start.y + 'px'
box.style.left = start.x + 'px'
container.prepend(box)
self.intersection(box)
}
function drag (e) {
end = getCoords(e)
const dimensions = getDimensions(start, end)
if (end.x < start.x) {
box.style.left = end.x + 'px'
}
if (end.y < start.y) {
box.style.top = end.y + 'px'
}
box.style.width = dimensions.width + 'px'
box.style.height = dimensions.height + 'px'
self.intersection(box)
}
function endDrag () {
start = { x: 0, y: 0 }
end = { x: 0, y: 0 }
box.style.width = 0
box.style.height = 0
document.removeEventListener('mousemove', drag)
document.removeEventListener('touchmove', touchMove)
box.remove()
}
container.addEventListener('mousedown', startDrag)
container.addEventListener('touchstart', touchStart)
document.addEventListener('mouseup', endDrag)
document.addEventListener('touchend', endDrag)
this.$once('on:destroy', () => {
container.removeEventListener('mousedown', startDrag)
container.removeEventListener('touchstart', touchStart)
document.removeEventListener('mouseup', endDrag)
document.removeEventListener('touchend', endDrag)
})
},
methods: {
createBox () {
const box = document.createElement('div')
box.setAttribute('data-drag-box-component', '')
box.style.position = 'absolute'
box.style.backgroundColor = this.color
box.style.opacity = this.opacity
box.style.zIndex = 1000
return box
},
intersection (box) {
const { children } = this
const rect = box.getBoundingClientRect()
const intersected = []
for (let i = 0; i < children.length; i++) {
if (collisionCheck(rect, children[i].getBoundingClientRect())) {
const attr = children[i].getAttribute(this.attribute)
if (children[i].hasAttribute(this.attribute)) {
intersected.push(attr)
}
}
}
if (
JSON.stringify([...intersected]) !==
JSON.stringify([...this.intersected])
) { this.intersected = intersected }
}
}
}
</script>
+36 -34
View File
@@ -3,46 +3,48 @@
</template>
<script>
export default {
name: 'mo-dragger',
mounted () {
this.preventDefault = ev => ev.preventDefault()
let count = 0
this.onDragEnter = (ev) => {
if (count === 0) {
this.$store.dispatch('app/showAddTaskDialog', 'torrent')
import { ADD_TASK_TYPE } from '@shared/constants'
export default {
name: 'mo-dragger',
mounted () {
this.preventDefault = ev => ev.preventDefault()
let count = 0
this.onDragEnter = (ev) => {
if (count === 0) {
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
}
count++
}
count++
}
this.onDragLeave = (ev) => {
count--
if (count === 0) {
this.$store.dispatch('app/hideAddTaskDialog')
this.onDragLeave = (ev) => {
count--
if (count === 0) {
this.$store.dispatch('app/hideAddTaskDialog')
}
}
}
this.onDrop = (ev) => {
count = 0
this.onDrop = (ev) => {
count = 0
const fileList = [...ev.dataTransfer.files]
.map(item => ({ raw: item, name: item.name }))
.filter(item => /\.torrent$/.test(item.name))
if (!fileList.length) {
this.$msg.error(this.$t('task.select-torrent'))
const fileList = [...ev.dataTransfer.files]
.map(item => ({ raw: item, name: item.name }))
.filter(item => /\.torrent$/.test(item.name))
if (!fileList.length) {
this.$msg.error(this.$t('task.select-torrent'))
}
}
}
document.addEventListener('dragover', this.preventDefault)
document.body.addEventListener('dragenter', this.onDragEnter)
document.body.addEventListener('dragleave', this.onDragLeave)
document.body.addEventListener('drop', this.onDrop)
},
destroyed () {
document.removeEventListener('dragover', this.preventDefault)
document.body.removeEventListener('dragenter', this.onDragEnter)
document.body.removeEventListener('dragleave', this.onDragLeave)
document.body.removeEventListener('drop', this.onDrop)
document.addEventListener('dragover', this.preventDefault)
document.body.addEventListener('dragenter', this.onDragEnter)
document.body.addEventListener('dragleave', this.onDragLeave)
document.body.addEventListener('drop', this.onDrop)
},
destroyed () {
document.removeEventListener('dragover', this.preventDefault)
document.body.removeEventListener('dragenter', this.onDragEnter)
document.body.removeEventListener('dragleave', this.onDragLeave)
document.body.removeEventListener('drop', this.onDrop)
}
}
}
</script>
+153 -153
View File
@@ -58,171 +58,171 @@
</style>
<script>
let icons = {}
const icons = {}
export default {
name: 'fa-icon',
props: {
name: {
type: String,
validator (val) {
if (val && !(val in icons)) {
console.warn(`Invalid prop: prop "name" is referring to an unregistered icon "${val}".` +
`\nPlease make sure you have imported this icon before using it.`)
export default {
name: 'fa-icon',
props: {
name: {
type: String,
validator (val) {
if (val && !(val in icons)) {
console.warn(`Invalid prop: prop "name" is referring to an unregistered icon "${val}".` +
'\nPlease make sure you have imported this icon before using it.')
return false
}
return true
}
},
scale: [Number, String],
spin: Boolean,
inverse: Boolean,
pulse: Boolean,
flip: {
validator (val) {
return val === 'horizontal' || val === 'vertical'
}
},
label: String
},
data () {
return {
x: false,
y: false,
childrenWidth: 0,
childrenHeight: 0,
outerScale: 1
}
},
computed: {
normalizedScale () {
let scale = this.scale
scale = typeof scale === 'undefined' ? 1 : Number(scale)
if (isNaN(scale) || scale <= 0) {
console.warn('Invalid prop: prop "scale" should be a number over 0.', this)
return this.outerScale
}
return scale * this.outerScale
},
klass () {
return {
'fa-icon': true,
'fa-spin': this.spin,
'fa-flip-horizontal': this.flip === 'horizontal',
'fa-flip-vertical': this.flip === 'vertical',
'fa-inverse': this.inverse,
'fa-pulse': this.pulse,
[this.$options.name]: true
}
},
icon () {
if (this.name) {
return icons[this.name]
}
return null
},
box () {
if (this.icon) {
return `0 0 ${this.icon.width} ${this.icon.height}`
}
return `0 0 ${this.width} ${this.height}`
},
ratio () {
if (!this.icon) {
return 1
}
const { width, height } = this.icon
return Math.max(width, height) / 16
},
width () {
return this.childrenWidth || (this.icon && this.icon.width / this.ratio * this.normalizedScale) || 0
},
height () {
return this.childrenHeight || (this.icon && this.icon.height / this.ratio * this.normalizedScale) || 0
},
style () {
if (this.normalizedScale === 1) {
return false
}
return true
return {
fontSize: this.normalizedScale + 'em'
}
},
raw () {
// generate unique id for each icon's SVG element with ID
if (!this.icon || !this.icon.raw) {
return null
}
let raw = this.icon.raw
const ids = {}
raw = raw.replace(/\s(?:xml:)?id=(["']?)([^"')\s]+)\1/g, (match, quote, id) => {
const uniqueId = getId()
ids[id] = uniqueId
return ` id="${uniqueId}"`
})
raw = raw.replace(/#(?:([^'")\s]+)|xpointer\(id\((['"]?)([^')]+)\2\)\))/g, (match, rawId, _, pointerId) => {
const id = rawId || pointerId
if (!id || !ids[id]) {
return match
}
return `#${ids[id]}`
})
return raw
}
},
scale: [Number, String],
spin: Boolean,
inverse: Boolean,
pulse: Boolean,
flip: {
validator (val) {
return val === 'horizontal' || val === 'vertical'
mounted () {
if (!this.name && this.$children.length === 0) {
console.warn('Invalid prop: prop "name" is required.')
return
}
},
label: String
},
data () {
return {
x: false,
y: false,
childrenWidth: 0,
childrenHeight: 0,
outerScale: 1
}
},
computed: {
normalizedScale () {
let scale = this.scale
scale = typeof scale === 'undefined' ? 1 : Number(scale)
if (isNaN(scale) || scale <= 0) {
console.warn(`Invalid prop: prop "scale" should be a number over 0.`, this)
return this.outerScale
}
return scale * this.outerScale
},
klass () {
return {
'fa-icon': true,
'fa-spin': this.spin,
'fa-flip-horizontal': this.flip === 'horizontal',
'fa-flip-vertical': this.flip === 'vertical',
'fa-inverse': this.inverse,
'fa-pulse': this.pulse,
[this.$options.name]: true
}
},
icon () {
if (this.name) {
return icons[this.name]
}
return null
},
box () {
if (this.icon) {
return `0 0 ${this.icon.width} ${this.icon.height}`
return
}
return `0 0 ${this.width} ${this.height}`
},
ratio () {
if (!this.icon) {
return 1
}
let { width, height } = this.icon
return Math.max(width, height) / 16
},
width () {
return this.childrenWidth || (this.icon && this.icon.width / this.ratio * this.normalizedScale) || 0
},
height () {
return this.childrenHeight || (this.icon && this.icon.height / this.ratio * this.normalizedScale) || 0
},
style () {
if (this.normalizedScale === 1) {
return false
}
return {
fontSize: this.normalizedScale + 'em'
}
},
raw () {
// generate unique id for each icon's SVG element with ID
if (!this.icon || !this.icon.raw) {
return null
}
let raw = this.icon.raw
let ids = {}
raw = raw.replace(/\s(?:xml:)?id=(["']?)([^"')\s]+)\1/g, (match, quote, id) => {
let uniqueId = getId()
ids[id] = uniqueId
return ` id="${uniqueId}"`
let width = 0
let height = 0
this.$children.forEach(child => {
child.outerScale = this.normalizedScale
width = Math.max(width, child.width)
height = Math.max(height, child.height)
})
raw = raw.replace(/#(?:([^'")\s]+)|xpointer\(id\((['"]?)([^')]+)\2\)\))/g, (match, rawId, _, pointerId) => {
let id = rawId || pointerId
if (!id || !ids[id]) {
return match
this.childrenWidth = width
this.childrenHeight = height
this.$children.forEach(child => {
child.x = (width - child.width) / 2
child.y = (height - child.height) / 2
})
},
register (data) {
for (const name in data) {
const icon = data[name]
if (!icon.paths) {
icon.paths = []
}
if (icon.d) {
icon.paths.push({ d: icon.d })
}
return `#${ids[id]}`
})
if (!icon.polygons) {
icon.polygons = []
}
if (icon.points) {
icon.polygons.push({ points: icon.points })
}
return raw
}
},
mounted () {
if (!this.name && this.$children.length === 0) {
console.warn(`Invalid prop: prop "name" is required.`)
return
}
if (this.icon) {
return
}
let width = 0
let height = 0
this.$children.forEach(child => {
child.outerScale = this.normalizedScale
width = Math.max(width, child.width)
height = Math.max(height, child.height)
})
this.childrenWidth = width
this.childrenHeight = height
this.$children.forEach(child => {
child.x = (width - child.width) / 2
child.y = (height - child.height) / 2
})
},
register (data) {
for (let name in data) {
let icon = data[name]
if (!icon.paths) {
icon.paths = []
}
if (icon.d) {
icon.paths.push({ d: icon.d })
icons[name] = icon
}
},
icons
}
if (!icon.polygons) {
icon.polygons = []
}
if (icon.points) {
icon.polygons.push({ points: icon.points })
}
icons[name] = icon
}
},
icons
}
let cursor = 0xd4937
function getId () {
return `fa-${(cursor++).toString(16)}`
}
let cursor = 0xd4937
function getId () {
return `fa-${(cursor++).toString(16)}`
}
</script>
@@ -0,0 +1,18 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'arrow-down': {
'width': 24,
'height': 24,
'raw': `<g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<line data-cap="butt" data-color="color-2" fill="none" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="19,15 12,22 5,15 "/>
</g>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+18
View File
@@ -0,0 +1,18 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'arrow-up': {
'width': 24,
'height': 24,
'raw': `<g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
<line data-cap="butt" data-color="color-2" fill="none" stroke-miterlimit="10" x1="12" y1="22" x2="12" y2="2"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="5,9 12,2 19,9 "/>
</g>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+20
View File
@@ -0,0 +1,20 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'audio': {
'width': 24,
'height': 24,
'raw': `<rect x="4" y="3" width="16" height="18" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<line data-color="color-2" x1="1" y1="6" x2="1" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-color="color-2" x1="23" y1="6" x2="23" y2="18" fill="none" stroke-miterlimit="10"/>
<polyline data-cap="butt" points="10 15 10 8 16 8 16 14" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<circle data-stroke="none" cx="9" cy="15" r="2" stroke="none"/>
<circle data-stroke="none" cx="15" cy="14" r="2" stroke="none"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+21
View File
@@ -0,0 +1,21 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'dice': {
'width': 24,
'height': 24,
'raw': `<polyline data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" points="17,23 17,7 1,7 "/>
<line data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" x1="17" y1="7" x2="23" y2="1"/>
<polygon fill="none" stroke="currentColor" stroke-miterlimit="10" points="17,23 23,17 23,1 7,1 1,7 1,23 "/>
<circle data-color="color-2" data-stroke="none" cx="12" cy="12" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="6" cy="12" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="12" cy="18" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>
<circle data-color="color-2" data-stroke="none" cx="6" cy="18" r="1" stroke-linejoin="miter" stroke-linecap="square" stroke="none"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+17
View File
@@ -0,0 +1,17 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'image': {
'width': 24,
'height': 24,
'raw': `<polyline data-cap="butt" data-color="color-2" points="1 20 6 14 10 18 17 10 23 17" fill="none" stroke-miterlimit="10"/>
<rect x="1" y="3" width="22" height="18" fill="none" stroke="currentColor" stroke-miterlimit="10"/>
<circle data-color="color-2" cx="9" cy="8" r="2" fill="none" stroke-miterlimit="10"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+2 -2
View File
@@ -4,8 +4,8 @@ Icon.register({
'menu-add': {
'width': 24,
'height': 24,
'raw': `<line fill="none" stroke="#fff" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22" />
<line fill="none" stroke="#fff" stroke-miterlimit="10" x1="22" y1="12" x2="2" y2="12" />`,
'raw': `<line fill="none" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22" />
<line fill="none" stroke-miterlimit="10" x1="22" y1="12" x2="2" y2="12" />`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
+20
View File
@@ -0,0 +1,20 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'node': {
'width': 24,
'height': 24,
'raw': `
<polyline data-cap="butt" fill="none" stroke-miterlimit="10" points="12,9 12,13 7.8,16.1 "/>
<line data-cap="butt" fill="none" stroke-miterlimit="10" x1="12" y1="13" x2="16.2" y2="16.1"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="12" cy="5" r="4"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="5" cy="19" r="4"/>
<circle fill="none" stroke="currentColor" stroke-miterlimit="10" cx="19" cy="19" r="4"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
@@ -0,0 +1,11 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'task-history': {
'width': 24,
'height': 24,
'paths': [{
'd': 'M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12s12-5.383,12-12S18.617,0,12,0z M19,13h-8V5h2v6h6V13z'
}]
}
})
@@ -0,0 +1,17 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'task-restart': {
'width': 24,
'height': 24,
'raw': `<path data-cap="butt" fill="none" stroke="currentColor" stroke-miterlimit="10" d="M6.121,20.121 C7.727,21.22,9.907,22,12,22c5.523,0,10-4.477,10-10c0-5.523-4.477-10-10-10C8.101,2,4.728,4.233,3.078,7.488"/>
<polyline fill="none" stroke="currentColor" stroke-miterlimit="10" points="2.278,1.588 3.078,7.488 9.078,6.688 "/>
<circle data-color="color-2" fill="none" stroke-miterlimit="10" cx="4" cy="18" r="3"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+22
View File
@@ -0,0 +1,22 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'video': {
'width': 24,
'height': 24,
'raw': `<line data-cap="butt" data-color="color-2" x1="2" y1="6" x2="22" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="12" y1="2" x2="12" y2="22" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="7" y1="2" x2="7" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="17" y1="2" x2="17" y2="6" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="2" y1="18" x2="22" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="7" y1="22" x2="7" y2="18" fill="none" stroke-miterlimit="10"/>
<line data-cap="butt" data-color="color-2" x1="17" y1="22" x2="17" y2="18" fill="none" stroke-miterlimit="10"/>
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-miterlimit="10"/>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+1 -2
View File
@@ -14,7 +14,6 @@
import { mapState } from 'vuex'
import AboutPanel from '@/components/About/AboutPanel'
import Aside from '@/components/Aside/Index'
import Subnav from '@/components/Subnav/Index'
import Speedometer from '@/components/Speedometer/Speedometer'
import AddTask from '@/components/Task/AddTask'
import TaskItemInfo from '@/components/Task/TaskItemInfo'
@@ -25,7 +24,6 @@
components: {
[AboutPanel.name]: AboutPanel,
[Aside.name]: Aside,
[Subnav.name]: Subnav,
[Speedometer.name]: Speedometer,
[AddTask.name]: AddTask,
[TaskItemInfo.name]: TaskItemInfo,
@@ -52,5 +50,6 @@
position: fixed;
right: 36px;
bottom: 24px;
z-index: 20;
}
</style>
+48 -35
View File
@@ -8,11 +8,10 @@
import api from '@/api'
import {
showItemInFolder,
addToRecentTask,
openDownloadDock,
showDownloadSpeedInDock
addToRecentTask
} from '@/components/Native/utils'
import {
bytesToSize,
getTaskName,
getTaskFullPath
} from '@shared/utils'
@@ -40,15 +39,16 @@
})
},
watch: {
downloadSpeed: function (val, oldVal) {
showDownloadSpeedInDock(val)
downloadSpeed (val, oldVal) {
const speed = val > 0 ? `${bytesToSize(val)}/s` : ''
this.$electron.ipcRenderer.send('event', 'download-speed-change', speed)
},
numActive: function (val, oldVal) {
numActive (val, oldVal) {
this.downloading = val > 0
},
downloading: function (val, oldVal) {
downloading (val, oldVal) {
if (val !== oldVal && this.isRenderer) {
this.$electron.ipcRenderer.send('download-status-change', val)
this.$electron.ipcRenderer.send('event', 'download-status-change', val)
}
}
},
@@ -59,7 +59,7 @@
console.warn(`fetchTaskItem fail: ${e.message}`)
})
},
onDownloadStart: function (event) {
onDownloadStart (event) {
this.$store.dispatch('task/fetchList')
this.$store.dispatch('app/resetInterval')
console.log('aria2 onDownloadStart', event)
@@ -71,7 +71,7 @@
this.$msg.info(message)
})
},
onDownloadPause: function (event) {
onDownloadPause (event) {
console.log('aria2 onDownloadPause')
const [{ gid }] = event
this.fetchTaskItem({ gid })
@@ -81,7 +81,7 @@
this.$msg.info(message)
})
},
onDownloadStop: function (event) {
onDownloadStop (event) {
console.log('aria2 onDownloadStop')
const [{ gid }] = event
this.fetchTaskItem({ gid })
@@ -91,17 +91,25 @@
this.$msg.info(message)
})
},
onDownloadError: function (event) {
console.log('aria2 onDownloadError', event)
onDownloadError (event) {
const [{ gid }] = event
this.fetchTaskItem({ gid })
.then((task) => {
const taskName = getTaskName(task)
const { errorCode, errorMessage } = task
console.error(`[Motrix] download error gid: ${gid}, #${errorCode}, ${errorMessage}`)
const message = this.$t('task.download-error-message', { taskName })
this.$msg.error(message)
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,
duration: 5000,
dangerouslyUseHTMLString: true,
message: `${message} ${link}`
})
})
},
onDownloadComplete: function (event) {
onDownloadComplete (event) {
console.log('aria2 onDownloadComplete')
this.$store.dispatch('task/fetchList')
const [{ gid }] = event
@@ -110,7 +118,7 @@
this.handleDownloadComplete(task, false)
})
},
onBtDownloadComplete: function (event) {
onBtDownloadComplete (event) {
console.log('aria2 onBtDownloadComplete')
this.$store.dispatch('task/fetchList')
const [{ gid }] = event
@@ -119,15 +127,16 @@
this.handleDownloadComplete(task, true)
})
},
handleDownloadComplete: function (task, isBT) {
handleDownloadComplete (task, isBT) {
const path = getTaskFullPath(task)
addToRecentTask(task)
openDownloadDock(path)
this.showTaskCompleteNotify(task, isBT, path)
addToRecentTask(task)
this.$electron.ipcRenderer.send('event', 'task-download-complete', task, path)
},
showTaskCompleteNotify: function (task, isBT, path) {
showTaskCompleteNotify (task, isBT, path) {
const taskName = getTaskName(task)
const message = isBT
? this.$t('task.bt-download-complete-message', { taskName })
@@ -142,11 +151,11 @@
return
}
/* eslint-disable no-new */
const notifyMessage = isBT
? this.$t('task.bt-download-complete-notify')
: this.$t('task.download-complete-notify')
/* eslint-disable no-new */
const notify = new Notification(notifyMessage, {
body: `${taskName}${tips}`
})
@@ -156,7 +165,7 @@
})
}
},
showTaskErrorNotify: function (task) {
showTaskErrorNotify (task) {
const taskName = getTaskName(task)
const message = this.$t('task.download-fail-message', { taskName })
@@ -171,7 +180,7 @@
body: taskName
})
},
bindEngineEvents: function () {
bindEngineEvents () {
api.client.on('onDownloadStart', this.onDownloadStart)
// api.client.on('onDownloadPause', this.onDownloadPause)
api.client.on('onDownloadStop', this.onDownloadStop)
@@ -179,7 +188,7 @@
api.client.on('onDownloadError', this.onDownloadError)
api.client.on('onBtDownloadComplete', this.onBtDownloadComplete)
},
unbindEngineEvents: function () {
unbindEngineEvents () {
api.client.removeListener('onDownloadStart', this.onDownloadStart)
// api.client.removeListener('onDownloadPause', this.onDownloadPause)
api.client.removeListener('onDownloadStop', this.onDownloadStop)
@@ -187,33 +196,37 @@
api.client.removeListener('onDownloadError', this.onDownloadError)
api.client.removeListener('onBtDownloadComplete', this.onBtDownloadComplete)
},
startPolling: function () {
startPolling () {
this.timer = setTimeout(() => {
this.polling()
this.startPolling()
}, this.interval)
},
polling: function () {
polling () {
this.$store.dispatch('app/fetchGlobalStat')
this.$store.dispatch('task/fetchList')
if (this.taskItemInfoVisible && this.currentTaskItem) {
this.$store.dispatch('task/fetchItem', this.currentTaskItem.gid)
}
},
stopPolling: function () {
stopPolling () {
clearTimeout(this.timer)
this.timer = null
}
},
created: function () {
this.$store.dispatch('app/fetchEngineInfo')
this.$store.dispatch('app/fetchEngineOptions')
this.startPolling()
created () {
this.bindEngineEvents()
},
destroyed: function () {
mounted () {
setTimeout(() => {
this.$store.dispatch('app/fetchEngineInfo')
this.$store.dispatch('app/fetchEngineOptions')
this.startPolling()
}, 100)
},
destroyed () {
this.$store.dispatch('task/saveSession')
this.unbindEngineEvents()
+2 -2
View File
@@ -31,14 +31,14 @@
created: function () {
this.bindIpcEvents()
// id of the menu item
let visibleStates = {}
const visibleStates = {}
if (is.mas()) {
visibleStates['app.check-for-updates'] = false
if (!this.enableEggFeatures) {
visibleStates['task.new-bt-task'] = false
}
}
this.$electron.ipcRenderer.send('update-menu-states', visibleStates, null, null)
this.$electron.ipcRenderer.send('command', 'application:change-menu-states', visibleStates, null, null)
},
destroyed: function () {
this.unbindIpcEvents()
@@ -18,11 +18,12 @@
onFolderClick: function () {
const self = this
this.$electron.remote.dialog.showOpenDialog({
properties: ['openDirectory']
}, (filePaths) => {
if (!filePaths) {
properties: ['openDirectory', 'createDirectory']
}).then(({ canceled, filePaths }) => {
if (canceled || filePaths.length === 0) {
return
}
const [path] = filePaths
self.$emit('selected', path)
})
@@ -6,6 +6,9 @@
<script>
import '@/components/Icons/folder'
import {
showItemInFolder
} from '@/components/Native/utils'
export default {
name: 'mo-show-in-folder',
@@ -21,7 +24,7 @@
if (!this.path) {
return
}
this.$electron.shell.showItemInFolder(this.path, {
showItemInFolder(this.path, {
errorMsg: this.$t('task.file-not-exist')
})
}
+2 -2
View File
@@ -75,8 +75,8 @@
z-index: 5100;
> li {
display: inline-block;
padding: 5px 10px;
margin: 0 5px;
padding: 5px 15px;
margin: 0;
color: $--titlebar-actions-color;
&:hover {
background-color: $--titlebar-actions-active-background;
+46 -28
View File
@@ -1,12 +1,13 @@
import is from 'electron-is'
import { existsSync } from 'fs'
import { access, constants } from 'fs'
import { Message } from 'element-ui'
import {
isMagnetTask,
getTaskFullPath,
bytesToSize
} from '@shared/utils'
import { LIGHT_THEME, DARK_THEME } from '@shared/constants'
import { APP_THEME, TASK_STATUS } from '@shared/constants'
const remote = is.renderer() ? require('electron').remote : {}
@@ -24,11 +25,16 @@ export function showItemInFolder (fullPath, { errorMsg }) {
if (!fullPath) {
return
}
const result = remote.shell.showItemInFolder(fullPath)
if (!result && errorMsg) {
Message.error(errorMsg)
}
return result
access(fullPath, constants.F_OK, (err) => {
console.log(`${fullPath} ${err ? 'does not exist' : 'exists'}`)
if (err) {
Message.error(errorMsg)
return
}
remote.shell.showItemInFolder(fullPath)
})
}
export function openItem (fullPath, { errorMsg }) {
@@ -42,40 +48,44 @@ export function openItem (fullPath, { errorMsg }) {
return result
}
export function moveTaskFilesToTrash (task, messages = {}) {
export function moveTaskFilesToTrash (task) {
/**
* 磁力链接任务 bittorrent但没有 bittorrent.info
* 在没下完变成BT任务之前 path 不是一个完整路径
* 未避免误删所在目录所以删除时直接返回 true
* For magnet link tasks, there is bittorrent, but there is no bittorrent.info.
* The path is not a complete path before it becomes a BT task.
* In order to avoid accidentally deleting the directory
* where the task is located, it directly returns true when deleting.
*/
if (isMagnetTask(task)) {
return true
}
const { pathErrorMsg, delFailMsg, delConfigFailMsg } = messages
const { dir } = task
const { dir, status } = task
const path = getTaskFullPath(task)
if (!path || dir === path) {
if (pathErrorMsg) {
Message.error(pathErrorMsg)
}
return false
throw new Error('task.file-path-error')
}
const deleteResult1 = remote.shell.moveItemToTrash(path)
if (!deleteResult1 && delFailMsg) {
Message.error(delFailMsg)
let deleteResult1 = true
access(path, constants.F_OK, (err) => {
console.log(`${path} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult1 = remote.shell.moveItemToTrash(path)
}
})
// There is no configuration file for the completed task.
if (status === TASK_STATUS.COMPLETE) {
return deleteResult1
}
let deleteResult2 = true
const extraFilePath = `${path}.aria2`
const isExtraExist = existsSync(extraFilePath)
if (isExtraExist) {
deleteResult2 = remote.shell.moveItemToTrash(extraFilePath)
if (!deleteResult2 && delConfigFailMsg) {
Message.error(delConfigFailMsg)
access(extraFilePath, constants.F_OK, (err) => {
console.log(`${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult2 = remote.shell.moveItemToTrash(extraFilePath)
}
}
})
return deleteResult1 && deleteResult2
}
@@ -125,10 +135,18 @@ export function clearRecentTasks () {
}
export function getSystemTheme () {
let result = LIGHT_THEME
let result = APP_THEME.LIGHT
if (!is.macOS()) {
return result
}
result = remote.systemPreferences.isDarkMode() ? DARK_THEME : LIGHT_THEME
result = remote.nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
export const openExternal = (url, options) => {
if (!url) {
return
}
remote.shell.openExternal(url, options)
}
+403 -110
View File
@@ -1,7 +1,12 @@
<template>
<el-container class="content panel" direction="vertical">
<el-header class="panel-header" height="84">
<h4>{{ title }}</h4>
<h4 class="hidden-xs-only">{{ title }}</h4>
<mo-subnav-switcher
:title="title"
:subnavs="subnavs"
class="hidden-sm-and-up"
/>
</el-header>
<el-main class="panel-content">
<el-form
@@ -10,36 +15,39 @@
label-position="right"
size="mini"
:model="form"
:rules="rules">
<el-form-item :label="`${$t('preferences.appearance')}: `" :label-width="formLabelWidth">
:rules="rules"
>
<el-form-item
:label="`${$t('preferences.auto-update')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
<mo-theme-switcher
v-model="form.theme"
@change="handleThemeChange"
/>
</el-col>
<el-col v-if="showHideAppMenuOption" class="form-item-sub" :span="16">
<el-checkbox v-model="form.hideAppMenu">
{{ $t('preferences.hide-app-menu') }}
<el-checkbox v-model="form.autoCheckUpdate">
{{ $t('preferences.auto-check-update') }}
</el-checkbox>
<div
class="el-form-item__info"
style="margin-top: 8px;"
v-if="form.lastCheckUpdateTime !== 0"
>
{{
$t('preferences.last-check-update-time') + ': ' +
(
form.lastCheckUpdateTime !== 0 ?
new Date(form.lastCheckUpdateTime).toLocaleString() :
new Date().toLocaleString()
)
}}
<span class="action-link" @click.prevent="onCheckUpdateClick">
{{ $t('app.check-updates-now') }}
</span>
</div>
</el-col>
</el-form-item>
<el-form-item :label="`${$t('preferences.language')}: `" :label-width="formLabelWidth">
<el-col class="form-item-sub" :span="16">
<el-select
v-model="form.locale"
@change="handleLocaleChange"
:placeholder="$t('preferences.change-language')">
<el-option
v-for="item in locales"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-form-item :label="`${$t('preferences.proxy')}: `" :label-width="formLabelWidth">
<el-form-item
:label="`${$t('preferences.proxy')}: `"
:label-width="formLabelWidth"
>
<el-switch
v-model="form.useProxy"
:active-text="$t('preferences.use-proxy')"
@@ -47,7 +55,11 @@
>
</el-switch>
</el-form-item>
<el-form-item label="" :label-width="formLabelWidth" v-if="form.useProxy">
<el-form-item
:label-width="formLabelWidth"
v-if="form.useProxy"
style="margin-top: -16px;"
>
<el-col class="form-item-sub" :span="16">
<el-input
placeholder="[http://][USER:PASSWORD@]HOST[:PORT]"
@@ -55,52 +67,176 @@
v-model="form.allProxyBackup">
</el-input>
</el-col>
</el-form-item>
<el-form-item :label="`${$t('preferences.bt-tracker')}: `" :label-width="formLabelWidth">
<div class="bt-tracker">
<el-col class="form-item-sub" :span="20">
<el-input
type="textarea"
:autosize="{ minRows: 3, maxRows: 5 }"
rows="2"
auto-complete="off"
:placeholder="`${$t('preferences.no-proxy-input-tips')}`"
v-model="form.noProxy">
</el-input>
<div class="el-form-item__info" style="margin-top: 8px;">
<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/Proxy" rel="noopener noreferrer">
{{ $t('preferences.proxy-tips') }}
<mo-icon name="link" width="12" height="12" />
</a>
</div>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.bt-tracker')}: `"
:label-width="formLabelWidth"
>
<div class="form-item-sub bt-tracker">
<el-row :gutter="10" style="line-height: 0;">
<el-col :span="20">
<div class="track-source">
<el-select
class="select-track-source"
v-model="form.trackerSource"
multiple
>
<el-option-group
v-for="group in trackerSourceOptions"
:key="group.label"
:label="group.label"
>
<el-option
v-for="item in group.options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-option-group>
</el-select>
</div>
</el-col>
<el-col :span="3">
<div class="sync-tracker">
<el-tooltip
class="item"
effect="dark"
:content="$t('preferences.sync-tracker-tips')"
placement="bottom"
>
<el-button
@click="syncTrackerFromSource"
class="sync-tracker-btn"
>
<mo-icon
name="refresh"
width="12"
height="12"
:spin="true"
v-if="trackerSyncing"
/>
<mo-icon name="sync" width="12" height="12" v-else />
</el-button>
</el-tooltip>
</div>
</el-col>
</el-row>
<el-input
type="textarea"
rows="3"
auto-complete="off"
:placeholder="`${$t('preferences.bt-tracker-input-tips')}`"
v-model="form.btTracker">
</el-input>
<div class="sync-tracker">
<el-tooltip
class="item"
effect="dark"
:content="$t('preferences.sync-tracker-tips')"
placement="bottom"
>
<el-button
@click="syncTrackerFromGitHub"
>
<mo-icon
name="refresh"
width="12"
height="12"
:spin="true"
v-if="isSyncTracker"
/>
<mo-icon name="sync" width="12" height="12" v-else />
</el-button>
</el-tooltip>
<div class="el-form-item__info" style="margin-top: 8px;">
{{ $t('preferences.bt-tracker-tips') }}
<a target="_blank" href="https://github.com/ngosang/trackerslist" rel="noopener noreferrer">
ngosang/trackerslist
<mo-icon name="link" width="12" height="12" />
</a>
<a target="_blank" href="https://github.com/XIU2/TrackersListCollection" rel="noopener noreferrer">
XIU2/TrackersListCollection
<mo-icon name="link" width="12" height="12" />
</a>
</div>
</div>
<div class="el-form-item__info" style="margin-top: 8px;">
{{ $t('preferences.bt-tracker-tips') }}
<a target="_blank" href="https://github.com/ngosang/trackerslist" rel="noopener noreferrer">
https://github.com/ngosang/trackerslist
<mo-icon name="link" width="12" height="12" />
</a>
<div class="form-item-sub">
<el-checkbox v-model="form.autoSyncTracker">
{{ $t('preferences.auto-sync-tracker') }}
</el-checkbox>
<div class="el-form-item__info" style="margin-top: 8px;" v-if="form.lastSyncTrackerTime > 0">
{{ new Date(form.lastSyncTrackerTime).toLocaleString() }}
</div>
</div>
</el-form-item>
<el-form-item :label="`${$t('preferences.developer')}: `" :label-width="formLabelWidth">
<el-form-item
:label="`${$t('preferences.port')}: `"
:label-width="formLabelWidth"
>
<el-row style="margin-bottom: 8px;">
<el-col class="form-item-sub" :span="10">
<el-switch
v-model="form.enableUpnp"
active-text="UPnP"
>
</el-switch>
</el-col>
</el-row>
<el-row style="margin-bottom: 8px;">
<el-col class="form-item-sub" :span="10">
{{ $t('preferences.bt-port') }}
<el-input
placeholder="BT Port"
:maxlength="8"
v-model="form.listenPort"
>
<i slot="append" @click.prevent="onPortDiceClick">
<mo-icon name="dice" width="12" height="12" />
</i>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col class="form-item-sub" :span="10">
{{ $t('preferences.dht-port') }}
<el-input
placeholder="DHT Port"
:maxlength="8"
v-model="form.dhtListenPort"
>
<i slot="append" @click.prevent="onDhtPortDiceClick">
<mo-icon name="dice" width="12" height="12" />
</i>
</el-input>
</el-col>
</el-row>
</el-form-item>
<el-form-item
:label="`${$t('preferences.download-protocol')}: `"
:label-width="formLabelWidth"
>
{{ $t('preferences.protocols-default-client') }}
<el-col class="form-item-sub" :span="24">
<el-switch
v-model="form.protocols.magnet"
:active-text="$t('preferences.protocols-magnet')"
@change="(val) => onProtocolsChange('magnet', val)"
>
</el-switch>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-switch
v-model="form.protocols.thunder"
:active-text="$t('preferences.protocols-thunder')"
@change="(val) => onProtocolsChange('thunder', val)"
>
</el-switch>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.security')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.mock-user-agent') }}
<el-input
type="textarea"
:autosize="{ minRows: 2, maxRows: 3 }"
rows="2"
auto-complete="off"
placeholder="User-Agent"
v-model="form.userAgent">
@@ -111,12 +247,36 @@
<el-button @click="() => changeUA('du')">du</el-button>
</el-button-group>
</el-col>
<el-col class="form-item-sub" :span="18">
{{ $t('preferences.rpc-secret') }}
<el-input
:show-password="hideRpcSecret"
placeholder="RPC Secret"
:maxlength="24"
v-model="form.rpcSecret"
>
<i slot="append" @click.prevent="onDiceClick">
<mo-icon name="dice" width="12" height="12" />
</i>
</el-input>
<div class="el-form-item__info" style="margin-top: 8px;">
<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/RPC" rel="noopener noreferrer">
{{ $t('preferences.rpc-secret-tips') }}
<mo-icon name="link" width="12" height="12" />
</a>
</div>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.developer')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.app-log-path') }}
<el-input placeholder="" disabled v-model="logPath">
<mo-show-in-folder
slot="append"
v-if="isRenderer()"
v-if="isRenderer"
:path="logPath"
/>
</el-input>
@@ -126,7 +286,7 @@
<el-input placeholder="" disabled v-model="sessionPath">
<mo-show-in-folder
slot="append"
v-if="isRenderer()"
v-if="isRenderer"
:path="sessionPath"
/>
</el-input>
@@ -139,8 +299,17 @@
</el-form-item>
</el-form>
<div class="form-actions">
<el-button type="primary" @click="submitForm('advancedForm')">{{ $t('preferences.save') }}</el-button>
<el-button @click="resetForm('advancedForm')">{{ $t('preferences.discard') }}</el-button>
<el-button
type="primary"
@click="submitForm('advancedForm')"
>
{{ $t('preferences.save') }}
</el-button>
<el-button
@click="resetForm('advancedForm')"
>
{{ $t('preferences.discard') }}
</el-button>
</div>
</el-main>
</el-container>
@@ -149,15 +318,24 @@
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import ThemeSwitcher from '@/components/Preference/ThemeSwitcher'
import { cloneDeep } from 'lodash'
import randomize from 'randomatic'
import * as clipboard from 'clipboard-polyfill'
import ShowInFolder from '@/components/Native/ShowInFolder'
import SubnavSwitcher from '@/components/Subnav/SubnavSwitcher'
import userAgentMap from '@shared/ua'
import { availableLanguages, getLanguage } from '@shared/locales'
import { getLocaleManager } from '@/components/Locale'
import { trackerSourceOptions } from '@shared/constants'
import {
buildRpcUrl,
calcFormLabelWidth,
checkIsNeedRestart,
convertCommaToLine,
convertLineToComma
convertLineToComma,
diffConfig,
getRandomInt
} from '@shared/utils'
import { convertTrackerDataToLine } from '@shared/utils/tracker'
import '@/components/Icons/dice'
import '@/components/Icons/sync'
import '@/components/Icons/refresh'
@@ -165,20 +343,42 @@
const {
allProxy,
allProxyBackup,
autoCheckUpdate,
autoSyncTracker,
btTracker,
dhtListenPort,
enableUpnp,
hideAppMenu,
locale,
theme,
lastCheckUpdateTime,
lastSyncTrackerTime,
listenPort,
noProxy,
protocols,
rpcListenPort,
rpcSecret,
trackerSource,
useProxy,
userAgent
} = config
const result = {
allProxy,
allProxyBackup,
autoCheckUpdate,
autoSyncTracker,
btTracker: convertCommaToLine(btTracker),
dhtListenPort,
enableUpnp,
hideAppMenu,
locale,
theme,
lastCheckUpdateTime,
lastSyncTrackerTime,
listenPort,
noProxy: convertCommaToLine(noProxy),
protocols: {
...protocols
},
rpcListenPort,
rpcSecret,
trackerSource,
useProxy,
userAgent
}
@@ -188,25 +388,47 @@
export default {
name: 'mo-preference-advanced',
components: {
[ThemeSwitcher.name]: ThemeSwitcher,
[SubnavSwitcher.name]: SubnavSwitcher,
[ShowInFolder.name]: ShowInFolder
},
data: function () {
data () {
const { locale } = this.$store.state.preference.config
const form = initialForm(this.$store.state.preference.config)
const formOriginal = cloneDeep(form)
return {
formLabelWidth: '23%',
form: initialForm(this.$store.state.preference.config),
isSyncTracker: false,
form,
formLabelWidth: calcFormLabelWidth(locale),
formOriginal,
hideRpcSecret: true,
rules: {},
color: '#c00',
locales: availableLanguages
trackerSourceOptions,
trackerSyncing: false
}
},
computed: {
title: function () {
isRenderer () { return is.renderer() },
title () {
return this.$t('preferences.advanced')
},
showHideAppMenuOption: function () {
return is.windows() || is.linux()
subnavs: function () {
return [
{
key: 'basic',
title: this.$t('preferences.basic'),
route: '/preference/basic'
},
{
key: 'advanced',
title: this.$t('preferences.advanced'),
route: '/preference/advanced'
},
{
key: 'lab',
title: this.$t('preferences.lab'),
route: '/preference/lab'
}
]
},
...mapState('preference', {
config: state => state.config,
@@ -215,29 +437,44 @@
})
},
watch: {
'form.rpcSecret' (val) {
const url = buildRpcUrl({
port: this.form.rpcListenPort,
secret: val
})
clipboard.writeText(url)
}
},
methods: {
isRenderer: is.renderer,
handleLocaleChange (locale) {
const lng = getLanguage(locale)
getLocaleManager().changeLanguage(lng)
this.$electron.ipcRenderer.send('command', 'application:change-locale', lng)
onCheckUpdateClick () {
this.$electron.ipcRenderer.send('command', 'application:check-for-updates')
this.$msg.info(this.$t('app.checking-for-updates'))
this.$store.dispatch('preference/fetchPreference')
.then((config) => {
const { lastCheckUpdateTime } = config
this.form.lastCheckUpdateTime = lastCheckUpdateTime
})
},
handleThemeChange (theme) {
this.form.theme = theme
this.$electron.ipcRenderer.send('command', 'application:change-theme', theme)
},
syncTrackerFromGitHub () {
this.isSyncTracker = true
this.$store.dispatch('preference/fetchBtTracker')
syncTrackerFromSource () {
this.trackerSyncing = true
const { trackerSource } = this.form
this.$store.dispatch('preference/fetchBtTracker', trackerSource)
.then((data) => {
console.log('syncTrackerFromGitHub data====>', data)
this.form.btTracker = data
const tracker = convertTrackerDataToLine(data)
this.form.lastSyncTrackerTime = Date.now()
this.form.btTracker = tracker
})
.finally(() => {
this.isSyncTracker = false
this.trackerSyncing = false
})
},
onProtocolsChange (protocol, enabled) {
const { protocols } = this.form
this.form.protocols = {
...protocols,
[protocol]: enabled
}
},
onUseProxyChange (flag) {
this.form.allProxy = flag ? this.form.allProxyBackup : ''
},
@@ -251,6 +488,23 @@
}
this.form.userAgent = ua
},
onPortDiceClick () {
const port = getRandomInt(20000, 24999)
this.form.listenPort = port
},
onDhtPortDiceClick () {
const port = getRandomInt(25000, 29999)
this.form.dhtListenPort = port
},
onDiceClick () {
this.hideRpcSecret = false
const rpcSecret = randomize('Aa0', 12)
this.form.rpcSecret = rpcSecret
setTimeout(() => {
this.hideRpcSecret = true
}, 2000)
},
onFactoryResetClick () {
this.$electron.remote.dialog.showMessageBox({
type: 'warning',
@@ -258,32 +512,67 @@
message: this.$t('preferences.factory-reset-confirm'),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1
}, (buttonIndex) => {
if (buttonIndex === 0) {
}).then(({ response }) => {
if (response === 0) {
this.$electron.ipcRenderer.send('command', 'application:reset')
}
})
},
syncFormConfig () {
this.$store.dispatch('preference/fetchPreference')
.then((config) => {
this.form = initialForm(config)
this.formOriginal = cloneDeep(this.form)
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!')
console.log('[Motrix] preference form valid:', valid)
return false
}
const changed = diffConfig(this.formOriginal, this.form)
const data = {
...this.form,
btTracker: convertLineToComma(this.form.btTracker)
...changed,
protocols: {
...this.form.protocols
}
}
console.log('this.form===>', data)
const { btTracker, noProxy } = changed
if (btTracker) {
data.btTracker = convertLineToComma(btTracker)
}
if (noProxy) {
data.noProxy = convertLineToComma(noProxy)
}
console.log('[Motrix] preference changed data:', data)
this.$store.dispatch('preference/save', data)
if (this.isRenderer()) {
this.$electron.ipcRenderer.send('command', 'application:relaunch')
.then(() => {
this.$store.dispatch('app/fetchEngineOptions')
this.syncFormConfig()
this.$msg.success(this.$t('preferences.save-success-message'))
})
.catch((e) => {
this.$msg.success(this.$t('preferences.save-fail-message'))
})
if (this.isRenderer) {
this.$electron.ipcRenderer.send('command',
'application:setup-protocols-client', data.protocols)
if (checkIsNeedRestart(data)) {
this.$electron.ipcRenderer.send('command', 'application:relaunch')
}
}
})
},
resetForm (formName) {
this.form = initialForm(this.$store.state.preference.config)
this.syncFormConfig()
}
}
}
@@ -292,10 +581,14 @@
<style lang="scss">
.bt-tracker {
position: relative;
.sync-tracker {
position: absolute;
top: 8px;
right: 8px;
.sync-tracker-btn {
line-height: 0;
}
.track-source {
margin-bottom: 16px;
.select-track-source {
width: 100%;
}
}
}
.ua-group {
+301 -45
View File
@@ -1,7 +1,12 @@
<template>
<el-container class="content panel" direction="vertical">
<el-header class="panel-header" height="84">
<h4>{{ title }}</h4>
<h4 class="hidden-xs-only">{{ title }}</h4>
<mo-subnav-switcher
:title="title"
:subnavs="subnavs"
class="hidden-sm-and-up"
/>
</el-header>
<el-main class="panel-content">
<el-form
@@ -10,36 +15,132 @@
label-position="right"
size="mini"
:model="form"
:rules="rules">
<el-form-item :label="`${$t('preferences.startup')}: `" :label-width="formLabelWidth">
:rules="rules"
>
<el-form-item
:label="`${$t('preferences.appearance')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
<mo-theme-switcher
v-model="form.theme"
@change="handleThemeChange"
/>
</el-col>
<el-col v-if="showHideAppMenuOption" class="form-item-sub" :span="16">
<el-checkbox v-model="form.hideAppMenu">
{{ $t('preferences.hide-app-menu') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="16">
<el-checkbox v-model="form.autoHideWindow">
{{ $t('preferences.auto-hide-window') }}
</el-checkbox>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.run-mode')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
<el-select v-model="form.runMode">
<el-option
v-for="item in runModes"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.language')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="16">
<el-select
v-model="form.locale"
@change="handleLocaleChange"
:placeholder="$t('preferences.change-language')">
<el-option
v-for="item in locales"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.startup')}: `"
:label-width="formLabelWidth"
>
<el-col
class="form-item-sub"
:span="24"
v-if="!isLinux"
>
<el-checkbox v-model="form.openAtLogin">
{{ $t('preferences.open-at-login') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-checkbox v-model="form.keepWindowState">
{{ $t('preferences.keep-window-state') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-checkbox v-model="form.resumeAllWhenAppLaunched">
{{ $t('preferences.auto-resume-all') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-checkbox v-model="form.autoCheckUpdate">
{{ $t('preferences.auto-check-update') }}
</el-checkbox>
<div class="el-form-item__info" style="margin-top: 8px;" v-if="form.lastCheckUpdateTime !== 0">
{{ $t('preferences.last-check-update-time') + ': ' + (form.lastCheckUpdateTime !== 0 ? new
Date(form.lastCheckUpdateTime).toLocaleString() : new Date().toLocaleString()) }}
</div>
</el-col>
</el-form-item>
<el-form-item :label="`${$t('preferences.default-dir')}: `" :label-width="formLabelWidth">
<el-input placeholder="" v-model="downloadDir" :readonly="isMas()">
<el-form-item
:label="`${$t('preferences.default-dir')}: `"
:label-width="formLabelWidth"
>
<el-input placeholder="" v-model="form.dir" :readonly="isMas">
<mo-select-directory
v-if="isRenderer()"
v-if="isRenderer"
slot="append"
@selected="onDirectorySelected"
/>
</el-input>
<div class="el-form-item__info" v-if="isMas()" style="margin-top: 8px;">
<div class="el-form-item__info" v-if="isMas" style="margin-top: 8px;">
{{ $t('preferences.mas-default-dir-tips') }}
</div>
</el-form-item>
<el-form-item :label="`${$t('preferences.task-manage')}: `" :label-width="formLabelWidth">
<el-form-item
:label="`${$t('preferences.transfer-settings')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.transfer-speed-upload') }}
<el-select v-model="form.maxOverallUploadLimit">
<el-option
v-for="item in speedOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.transfer-speed-download') }}
<el-select v-model="form.maxOverallDownloadLimit">
<el-option
v-for="item in speedOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.task-manage')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.max-concurrent-downloads') }}
<el-input-number
@@ -53,10 +154,10 @@
<el-col class="form-item-sub" :span="24">
{{ $t('preferences.max-connection-per-server') }}
<el-input-number
v-model="form.split"
v-model="form.maxConnectionPerServer"
controls-position="right"
:min="1"
:max="form.maxConnectionPerServer"
:max="form.engineMaxConnectionPerServer"
:label="$t('preferences.max-connection-per-server')">
</el-input-number>
</el-col>
@@ -78,8 +179,17 @@
</el-form-item>
</el-form>
<div class="form-actions">
<el-button type="primary" @click="submitForm('basicForm')">{{ $t('preferences.save') }}</el-button>
<el-button @click="resetForm('basicForm')">{{ $t('preferences.discard') }}</el-button>
<el-button
type="primary"
@click="submitForm('basicForm')"
>
{{ $t('preferences.save') }}
</el-button>
<el-button
@click="resetForm('basicForm')"
>
{{ $t('preferences.discard') }}
</el-button>
</div>
</el-main>
</el-container>
@@ -88,32 +198,57 @@
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import { cloneDeep } from 'lodash'
import SubnavSwitcher from '@/components/Subnav/SubnavSwitcher'
import SelectDirectory from '@/components/Native/SelectDirectory'
import ThemeSwitcher from '@/components/Preference/ThemeSwitcher'
import { availableLanguages, getLanguage } from '@shared/locales'
import { getLocaleManager } from '@/components/Locale'
import { prettifyDir } from '@/components/Native/utils'
import {
calcFormLabelWidth,
checkIsNeedRestart,
diffConfig
} from '@shared/utils'
import { APP_RUN_MODE } from '@shared/constants'
const initialForm = (config) => {
const {
autoHideWindow,
dir,
split,
resumeAllWhenAppLaunched,
engineMaxConnectionPerServer,
hideAppMenu,
keepWindowState,
locale,
maxConcurrentDownloads,
maxConnectionPerServer,
taskNotification,
autoCheckUpdate,
maxOverallDownloadLimit,
maxOverallUploadLimit,
newTaskShowDownloading,
lastCheckUpdateTime
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
taskNotification,
theme
} = config
const result = {
dir,
split,
autoHideWindow,
continue: config.continue,
resumeAllWhenAppLaunched,
dir,
engineMaxConnectionPerServer,
hideAppMenu,
keepWindowState,
locale,
maxConcurrentDownloads,
maxConnectionPerServer,
taskNotification,
autoCheckUpdate,
maxOverallDownloadLimit,
maxOverallUploadLimit,
newTaskShowDownloading,
lastCheckUpdateTime
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
taskNotification,
theme
}
return result
}
@@ -121,20 +256,97 @@
export default {
name: 'mo-preference-basic',
components: {
[SelectDirectory.name]: SelectDirectory
[SubnavSwitcher.name]: SubnavSwitcher,
[SelectDirectory.name]: SelectDirectory,
[ThemeSwitcher.name]: ThemeSwitcher
},
data: function () {
data () {
const { locale } = this.$store.state.preference.config
const form = initialForm(this.$store.state.preference.config)
const formOriginal = cloneDeep(form)
return {
formLabelWidth: '23%',
form: initialForm(this.$store.state.preference.config),
form,
formLabelWidth: calcFormLabelWidth(locale),
formOriginal,
locales: availableLanguages,
rules: {}
}
},
computed: {
title: function () {
isRenderer () { return is.renderer() },
isMas () { return is.mas() },
isLinux () { return is.linux() },
title () {
return this.$t('preferences.basic')
},
downloadDir: function () {
runModes () {
return [
{
label: this.$t('preferences.run-mode-standard'),
value: 1
},
{
label: this.$t('preferences.run-mode-menu-bar'),
value: 2
}
]
},
speedOptions () {
return [
{
label: this.$t('preferences.transfer-speed-unlimited'),
value: 0
},
{
label: '128 KB/s',
value: '128K'
},
{
label: '256 KB/s',
value: '256K'
},
{
label: '512 KB/s',
value: '512K'
},
{
label: '1 MB/s',
value: '1M'
},
{
label: '5 MB/s',
value: '5M'
},
{
label: '10 MB/s',
value: '10M'
}
]
},
subnavs: function () {
return [
{
key: 'basic',
title: this.$t('preferences.basic'),
route: '/preference/basic'
},
{
key: 'advanced',
title: this.$t('preferences.advanced'),
route: '/preference/advanced'
},
{
key: 'lab',
title: this.$t('preferences.lab'),
route: '/preference/lab'
}
]
},
showHideAppMenuOption () {
return is.windows() || is.linux()
},
downloadDir () {
return prettifyDir(this.form.dir)
},
...mapState('preference', {
@@ -142,26 +354,70 @@
})
},
methods: {
isRenderer: is.renderer,
isMas: is.mas,
handleLocaleChange (locale) {
const lng = getLanguage(locale)
getLocaleManager().changeLanguage(lng)
this.$electron.ipcRenderer.send('command',
'application:change-locale', lng)
},
handleThemeChange (theme) {
this.form.theme = theme
this.$electron.ipcRenderer.send('command',
'application:change-theme', theme)
},
onDirectorySelected (dir) {
this.form.dir = dir
},
syncFormConfig () {
this.$store.dispatch('preference/fetchPreference')
.then((config) => {
this.form = initialForm(config)
this.formOriginal = cloneDeep(this.form)
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!')
console.log('[Motrix] preference form valid:', valid)
return false
}
this.$store.dispatch('preference/save', this.form)
if (this.isRenderer()) {
this.$electron.ipcRenderer.send('command', 'application:relaunch')
const { runMode, openAtLogin, autoHideWindow } = this.form
const changed = diffConfig(this.formOriginal, this.form)
const data = {
...changed
}
console.log('[Motrix] preference changed data:', data)
this.$store.dispatch('preference/save', data)
.then(() => {
this.$store.dispatch('app/fetchEngineOptions')
this.syncFormConfig()
this.$msg.success(this.$t('preferences.save-success-message'))
})
.catch(() => {
this.$msg.success(this.$t('preferences.save-fail-message'))
})
if (this.isRenderer) {
this.$electron.ipcRenderer.send('command',
'application:open-at-login', openAtLogin)
this.$electron.ipcRenderer.send('command',
'application:toggle-dock', runMode === APP_RUN_MODE.STANDARD)
this.$electron.ipcRenderer.send('command',
'application:auto-hide-window', autoHideWindow)
if (checkIsNeedRestart(data)) {
this.$electron.ipcRenderer.send('command',
'application:relaunch')
}
}
})
},
resetForm (formName) {
this.form = initialForm(this.$store.state.preference.config)
this.syncFormConfig()
}
}
}

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