Compare commits

...

227 Commits

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

* refactor: Complete translation into Arabic

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

* Partial Arabic Language support!

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

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

* Small fix

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

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

* Update all.js

* Update app.js

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

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

* fix: correction of the french locale

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

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

https://stackoverflow.com/questions/61268950/offscreen-converttoblob-very-slow
2020-06-21 21:10:22 +08:00
Dr_rOot 2af891aab8 chore: i18n tray speedometer 2020-06-21 16:23:07 +08:00
Dr_rOot 0d276de39b feat: preference tray speedometer setting 2020-06-21 16:12:35 +08:00
Dr_rOot 8a6beda335 feat: tray speedometer 2020-06-21 16:12:04 +08:00
Dr_rOot 68f1cdc4de chore: update copyright year 2020-06-21 15:30:28 +08:00
Dr_rOot 5290fcfa14 feat: dockmanager add handleSpeedChange 2020-06-21 15:21:06 +08:00
Dr_rOot d865b630a3 refactor: rename enable args 2020-06-21 15:19:50 +08:00
Dr_rOot 9d95d294cd refactor: rename store actions mutations 2020-06-21 13:47:02 +08:00
Dr_rOot ea46d9b3c6 refactor: load config fn use ipc invoke 2020-06-21 12:33:28 +08:00
Dr_rOot ef2c992af9 refactor: remove deprecated fns 2020-06-21 12:30:43 +08:00
Dr_rOot 64ee097c85 Merge pull request #718 from agalwood/feature/stop_seeding_202006201236
fix: stop bt task seeding notification #604
2020-06-20 18:29:48 +08:00
Dr_rOot 31517f93cb chore: i18n stopping seeding tip 2020-06-20 15:51:24 +08:00
Dr_rOot 6d54e557b9 fix: oddly stop seeding notify #604 2020-06-20 15:50:18 +08:00
Dr_rOot 3e61adcea3 Merge pull request #716 from agalwood/feature/url_protocol_202005290826
refactor: redesign motrix scheme
2020-06-20 12:34:52 +08:00
Dr_rOot 84d9ced137 Merge pull request #714 from agalwood/hotfix/hide_run_mode_202006192140
fix: preference show run mode only in macOS
2020-06-19 23:01:36 +08:00
Dr_rOot bc45ed9d5b refactor: add jsconfig 2020-06-19 21:44:53 +08:00
Dr_rOot 3d98104dbf fix: preference show run mode only in macOS 2020-06-19 21:42:00 +08:00
Dr_rOot fb6986ff6e docs: update readme app icon 2020-06-19 20:55:07 +08:00
Dr_rOot b8507570c6 refactor: rename initialForm to initForm 2020-06-12 17:22:38 +08:00
Dr_rOot cf3ef7606c fix: new task uri 2020-06-06 23:31:24 +08:00
Dr_rOot 12a9fa92c1 chore: bump version 2020-06-05 23:54:14 +08:00
Dr_rOot 0fd6617eba docs: update readme i18n 2020-06-05 23:52:34 +08:00
Dr_rOot 7ebbf929d5 fix: i18n no-confirm-before-delete-task missed 2020-06-05 23:49:27 +08:00
Dr_rOot 14223c2204 chore: bump version 2020-06-05 23:26:11 +08:00
Dr_rOot 2cfb6b1914 Merge pull request #689 from agalwood/hotfix/handle_url_202006051331
fix: open app with url handle fail
2020-06-05 14:04:27 +08:00
Dr_rOot c38cf80589 chore: update deps 2020-06-05 13:40:30 +08:00
Dr_rOot 3ee98eae1d fix: open app with resource url 2020-06-05 13:33:22 +08:00
Dr_rOot d2cff6356a Merge pull request #686 from NickoAilus/master
fix: edited Russian locale
2020-06-01 22:12:46 +08:00
NickoAilus 3ee432d683 Fixed Russian locale 2020-06-01 16:10:36 +03:00
Dmitry Kalinin 7f1822bb7e feat: Added bulgarian (bg) translations (#685)
* Added bulgarian (bg) translations

* Improve structure

* Fixed bugs

* Deleted comma
2020-06-01 17:33:46 +08:00
Dr_rOot 0223e691ff docs: readme i18n add vi 2020-05-31 22:57:56 +08:00
Duy–Thanh Doan 117dba9f37 feat: Add vietnamese translation (#680)
* Add vietnamese translation

* Update app.js and index.js

* Translated about.js

* Translated app.js

* first draft

* final draft

* final update - ready to review

* Update format
2020-05-31 22:52:26 +08:00
Dr_rOot e5241d09d7 refactor: protocol manager init 2020-05-29 17:28:03 +08:00
Dr_rOot c9ea1dece2 refactor: motrix url protocol 2020-05-29 17:02:43 +08:00
Dr_rOot 60d4108ddc refactor: promise all to allSettled 2020-05-29 17:00:47 +08:00
Dr_rOot ffc8de4766 refactor: add task utils 2020-05-29 16:58:35 +08:00
Dr_rOot 66f114bf72 Merge pull request #674 from agalwood/hotfix/upnp_cb_202005271904
fix: nat-api autoUpdate cb is not a function
2020-05-28 15:28:28 +08:00
Dr_rOot 44f00483f9 chore: update deps 2020-05-28 14:42:43 +08:00
Dr_rOot 1866e3fd4f fix: nat-api autoUpdate cb is not a function
replace nat-api to @motrix/nat-api

@motrix/nat-api forked from https://github.com/alxhotel/nat-api
2020-05-28 14:42:05 +08:00
Dr_rOot cfac883cbf chore: remove deprecated vue-html-loader 2020-05-28 12:11:30 +08:00
Dr_rOot 1431bab366 Merge pull request #670 from agalwood/hotfix/full_screen_preference_202005271103
fix: full screen save preference issue #663
2020-05-27 11:20:23 +08:00
Dr_rOot bb373947ff fix: full screen save preference issue #663 2020-05-27 11:06:51 +08:00
Dr_rOot 8f0dc65341 refactor: rename auto hide window 2020-05-27 11:06:31 +08:00
Dr_rOot 402185e1a2 chore: bump version 2020-05-25 22:35:09 +08:00
Dr_rOot d59b5c9841 Merge pull request #667 from agalwood/hotfix/refactor_202005231656
fix: upnp client & full screen issues
2020-05-25 22:15:36 +08:00
Dr_rOot eb442e4a7a fix: windows auto launch config lose 2020-05-25 21:55:23 +08:00
Dr_rOot e82e567069 refactor: time constants 2020-05-24 20:15:20 +08:00
Dr_rOot dc2876098d fix: upnp client is destroyed #662 2020-05-24 20:13:43 +08:00
Dr_rOot 6287942fbc fix: full screen mode no traffic light #663 2020-05-24 17:04:08 +08:00
Dr_rOot 389dc080b6 refactor: improve macOS fullscreen mode usability 2020-05-24 15:30:50 +08:00
Dr_rOot 45a23d73e6 fix: code error 2020-05-23 17:16:16 +08:00
Dr_rOot 8303dd305b refactor: fn rename & format 2020-05-23 17:03:45 +08:00
Dr_rOot a98292ce1e refactor: remove title bar buttons spacer 2020-05-23 16:57:01 +08:00
Dr_rOot 8df97b8433 fix: switching system theme will not auto switch app theme 2020-05-22 11:21:45 +08:00
Dr_rOot f29e95c9bc chore: add electron apps banner 2020-05-21 16:32:10 +08:00
Dr_rOot 52e045c886 docs: update readme for github markdown 2020-05-21 08:56:28 +08:00
Dr_rOot 4632c3619a docs: update readme 2020-05-21 08:54:01 +08:00
Dr_rOot 54c48be29b chore: bump version 2020-05-21 07:51:47 +08:00
Dr_rOot 301f1403df Merge pull request #658 from aarestu/translation/indonesia
feat: add Indonesian translations
2020-05-21 07:17:12 +08:00
Dr_rOot 88de047778 Merge pull request #659 from agalwood/hotfix/webpack_copy_202005210655
fix: webpack copy plugin path
2020-05-21 07:07:29 +08:00
Dr_rOot c119de78ce fix: webpack copy plugin path 2020-05-21 06:56:04 +08:00
Restu Suhendar 9d381e16da add translation Indonesia 2020-05-21 04:24:07 +07:00
Dr_rOot e4c6d6a9c0 Merge pull request #655 from agalwood/hotfix/linux_tray_202005202115
fix: linux tray context menu
2020-05-20 21:33:13 +08:00
Dr_rOot 5bb727cb6f chore: update deps 2020-05-20 21:16:35 +08:00
Dr_rOot 00f3209c68 refactor: improve logs 2020-05-20 21:16:11 +08:00
Dr_rOot 3f8b0e6f5f fix: tray popUpContextMenu linux not support 2020-05-20 21:16:01 +08:00
Dr_rOot 0543bc4e2c chore: bump version 2020-05-20 16:32:55 +08:00
Dr_rOot 9ded42f127 chore: update docs 2020-05-20 13:53:20 +08:00
Dr_rOot 834a1ad839 docs: update readme linux section 2020-05-20 11:30:50 +08:00
Dr_rOot 8f830f6a0d fix: too fast to toggle tracker syncing spinner 2020-05-20 11:10:59 +08:00
Dr_rOot ee111c92ee fix: too fast to shut down the engine 2020-05-20 11:09:50 +08:00
Dr_rOot 74c3a3c696 Merge pull request #650 from agalwood/hotfix/linux_tray_20200519
fix: linux tray not support right-click event
2020-05-19 22:06:45 +08:00
Dr_rOot 4d964ae16e fix: tray destroy remove listener 2020-05-19 21:50:36 +08:00
Dr_rOot be2c2e8383 docs: update readme snapcraft markdown 2020-05-19 12:23:31 +08:00
Dr_rOot 4e6164816f fix: linux tray not support right click 2020-05-19 12:23:00 +08:00
Dr_rOot fa7daf377f docs: update app screenshots 2020-05-18 14:24:06 +08:00
Dr_rOot afbe364525 docs: fix release badge 2020-05-18 13:43:36 +08:00
Dr_rOot ece5cea512 docs: update readme 2020-05-18 13:37:36 +08:00
Dr_rOot dc8fa5c647 chore: bump version v1.5.10 2020-05-15 21:54:16 +08:00
Dr_rOot 4fd96e7cae chore: bump version v1.5.9 2020-05-15 21:45:22 +08:00
Dr_rOot 1f22b5efba Merge pull request #640 from agalwood/hotfix/task_manager_202005151229
fix: save session delay too long
fix: hide delete selected tasks on stopped task list
2020-05-15 16:44:31 +08:00
Dr_rOot 801db0ed38 chore: upgrade electron 2020-05-15 12:34:39 +08:00
Dr_rOot f72577de65 fix: hide delete selected tasks on stopped task list 2020-05-15 12:33:58 +08:00
Dr_rOot dd21c76dea fix: save session delay too long 2020-05-15 12:29:51 +08:00
Dr_rOot 5a5a932735 Merge pull request #639 from agalwood/feature/no_confirm_202005062312
feat: no confirm before delete task
feat: add task select video include sub files
feat: preference support add custom tracker source #627
feat: Ctrl or ⌘ + Enter quick submit task #638
feat: improve task total length is zero ui #614
fix: split & max-connection-per-server failed #270
fix: npm build:clean script
2020-05-15 00:03:54 +08:00
Dr_rOot d63c1d431d feat: improve task total length is zero ui #614 2020-05-14 23:43:42 +08:00
Dr_rOot 1a12256c4c feat: Ctrl or ⌘ + Enter quick submit task 2020-05-14 23:40:37 +08:00
Dr_rOot 9394a0a79b chore: update deps & bump version 2020-05-14 22:27:44 +08:00
Dr_rOot 5e66205298 chore: disable electron security warnings 2020-05-14 22:26:34 +08:00
Dr_rOot eb796c3c5f feat: add task select video include sub files 2020-05-14 22:26:11 +08:00
Dr_rOot 9d17b3e9b2 fix: split & max-connection-per-server failed #270 2020-05-14 22:25:35 +08:00
Dr_rOot 7ee8d4fa0f refactor: engine instance listeners 2020-05-14 22:21:57 +08:00
Dr_rOot dba4bfb0e7 fix: perference sync tracker empty 2020-05-14 22:20:23 +08:00
Dr_rOot f9755f69cd chore: improve aria2 bt config 2020-05-14 22:20:02 +08:00
Dr_rOot 7a68e1bb82 fix: npm build:clean script 2020-05-14 22:14:18 +08:00
Dr_rOot 3b12f3960f refactor: code format 2020-05-14 22:13:56 +08:00
Dr_rOot dbe26dfa98 feat: preference support add custom tracker source 2020-05-14 22:12:34 +08:00
Dr_rOot 3be8952cff refactor: task actions commands 2020-05-14 22:08:59 +08:00
Dr_rOot dc5a368e00 Merge pull request #630 from agalwood/hotfix/aria2c_darwin_202005081709
fix: rebuild darwin aria2c v1.35

closed #628
2020-05-08 17:30:53 +08:00
Dr_rOot 11aca7ea0b chore: remove bt-force-encryption 2020-05-08 17:16:48 +08:00
Dr_rOot c35af2b109 fix: rebuild aria2c #628
static build script:
https://github.com/aria2/aria2/blob/master/
makerelease-osx.mk
2020-05-08 17:12:04 +08:00
Dr_rOot b33b505ccb refactor: upnp close client 2020-05-06 23:21:49 +08:00
Dr_rOot dd22cc0306 fix: remove enable egg features config 2020-05-06 23:19:58 +08:00
Dr_rOot 206eda08aa refactor: renderer native utils 2020-05-06 23:19:08 +08:00
Dr_rOot f6e29700d0 feat: no confirm before delete config 2020-05-06 23:16:50 +08:00
253 changed files with 17519 additions and 17818 deletions
+2 -3
View File
@@ -7,8 +7,7 @@ const chalk = require('chalk')
const del = require('del')
const { spawn } = require('child_process')
const webpack = require('webpack')
const Multispinner = require('multispinner')
const Multispinner = require('@motrix/multispinner')
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
@@ -28,7 +27,7 @@ if (process.env.BUILD_TARGET === 'clean') {
}
function clean () {
del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
del.sync(['release/*', '!.gitkeep'])
console.log(`\n${doneLog}\n`)
process.exit()
}
+5 -12
View File
@@ -7,6 +7,7 @@ const path = require('path')
const { dependencies, build } = require('../package.json')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
let mainConfig = {
entry: {
@@ -17,17 +18,6 @@ let mainConfig = {
],
module: {
rules: [
{
test: /\.(js)$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
{
test: /\.js$/,
use: 'babel-loader',
@@ -49,7 +39,10 @@ let mainConfig = {
path: path.join(__dirname, '../dist/electron')
},
plugins: [
new webpack.NoEmitOnErrorsPlugin()
new webpack.NoEmitOnErrorsPlugin(),
new ESLintPlugin({
formatter: require('eslint-friendly-formatter')
})
],
resolve: {
alias: {
+28 -45
View File
@@ -9,9 +9,10 @@ const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const ESLintPlugin = require('eslint-webpack-plugin');
/**
* List of node_modules to include in webpack bundle
@@ -23,7 +24,6 @@ const { VueLoaderPlugin } = require('vue-loader')
let whiteListedModules = ['vue']
let rendererConfig = {
devtool: '#cheap-module-eval-source-map',
entry: {
index: path.join(__dirname, '../src/renderer/pages/index/main.js')
},
@@ -33,14 +33,10 @@ let rendererConfig = {
module: {
rules: [
{
test: /\.(js|vue)$/,
enforce: 'pre',
exclude: /node_modules/,
test: /\.worker\.js$/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
loader: 'worker-loader',
options: { filename: '[name].js' }
}
},
{
@@ -52,7 +48,7 @@ let rendererConfig = {
loader: 'sass-loader',
options: {
implementation: require('sass'),
prependData: '@import "@/components/Theme/Variables.scss";',
additionalData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
@@ -70,7 +66,7 @@ let rendererConfig = {
options: {
implementation: require('sass'),
indentedSyntax: true,
prependData: '@import "@/components/Theme/Variables.scss";',
additionalData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
@@ -93,10 +89,6 @@ let rendererConfig = {
'css-loader'
]
},
{
test: /\.html$/,
use: 'vue-html-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
@@ -124,7 +116,7 @@ let rendererConfig = {
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]'
}
@@ -142,7 +134,7 @@ let rendererConfig = {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]'
}
@@ -160,45 +152,35 @@ let rendererConfig = {
filename: '[name].css',
chunkFilename: '[id].css'
}),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true,
discardComments: { removeAll: true }
}
}),
new HtmlWebpackPlugin({
title: 'Motrix',
filename: 'index.html',
chunks: ['index'],
template: path.resolve(__dirname, '../src/index.ejs'),
templateParameters(compilation, assets, options) {
return {
compilation: compilation,
webpack: compilation.getStats().toJson(),
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
options: options
},
process
}
},
// minify: {
// collapseWhitespace: true,
// removeAttributeQuotes: true,
// removeComments: true
// },
isBrowser: false,
isDev: process.env.NODE_ENV !== 'production',
nodeModules: devMode
? path.resolve(__dirname, '../node_modules')
: false
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
new webpack.NoEmitOnErrorsPlugin(),
new ESLintPlugin({
extensions: ['js', 'vue'],
formatter: require('eslint-friendly-formatter')
})
],
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
path: path.join(__dirname, '../dist/electron'),
globalObject: 'this',
publicPath: ''
},
resolve: {
alias: {
@@ -214,7 +196,8 @@ let rendererConfig = {
minimizer: [
new TerserPlugin({
extractComments: false,
})
}),
new CssMinimizerPlugin(),
],
},
}
@@ -223,6 +206,8 @@ let rendererConfig = {
* Adjust rendererConfig for development settings
*/
if (devMode) {
rendererConfig.devtool = 'eval-cheap-module-source-map'
rendererConfig.plugins.push(
new webpack.DefinePlugin({
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
@@ -234,16 +219,14 @@ if (devMode) {
* Adjust rendererConfig for production settings
*/
if (!devMode) {
rendererConfig.devtool = ''
rendererConfig.plugins.push(
new CopyWebpackPlugin([
{
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/electron/static'),
ignore: ['.*']
}
]),
globOptions: { ignore: [ '.*' ] }
}]
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
+34 -42
View File
@@ -9,9 +9,10 @@ const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const ESLintPlugin = require('eslint-webpack-plugin');
/**
* List of node_modules to include in webpack bundle
@@ -23,7 +24,6 @@ const { VueLoaderPlugin } = require('vue-loader')
let whiteListedModules = ['vue']
let webConfig = {
devtool: '#cheap-module-eval-source-map',
entry: {
index: path.join(__dirname, '../src/renderer/pages/index/main.js')
},
@@ -33,14 +33,10 @@ let webConfig = {
module: {
rules: [
{
test: /\.(js|vue)$/,
enforce: 'pre',
exclude: /node_modules/,
test: /\.worker\.js$/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
loader: 'worker-loader',
options: { filename: '[name].js' }
}
},
{
@@ -52,7 +48,7 @@ let webConfig = {
loader: 'sass-loader',
options: {
implementation: require('sass'),
prependData: '@import "@/components/Theme/Variables.scss";',
additionalData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
@@ -70,7 +66,7 @@ let webConfig = {
options: {
implementation: require('sass'),
indentedSyntax: true,
prependData: '@import "@/components/Theme/Variables.scss";',
additionalData: '@import "@/components/Theme/Variables.scss";',
sassOptions: {
includePaths:[__dirname, 'src']
}
@@ -117,7 +113,7 @@ let webConfig = {
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'imgs/[name].[ext]'
}
@@ -127,7 +123,7 @@ let webConfig = {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
options: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
@@ -141,34 +137,18 @@ let webConfig = {
filename: '[name].css',
chunkFilename: '[id].css'
}),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true,
discardComments: { removeAll: true }
}
}),
new HtmlWebpackPlugin({
title: 'Motrix',
filename: 'index.html',
chunks: ['index'],
template: path.resolve(__dirname, '../src/index.ejs'),
templateParameters(compilation, assets, options) {
return {
compilation: compilation,
webpack: compilation.getStats().toJson(),
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
options: options
},
process
}
},
// minify: {
// collapseWhitespace: true,
// removeAttributeQuotes: true,
// removeComments: true
// },
isBrowser: true,
isDev: process.env.NODE_ENV !== 'production',
nodeModules: devMode
? path.resolve(__dirname, '../node_modules')
: false
@@ -177,11 +157,17 @@ let webConfig = {
'process.env.IS_WEB': 'true'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
new webpack.NoEmitOnErrorsPlugin(),
new ESLintPlugin({
extensions: ['js', 'vue'],
formatter: require('eslint-friendly-formatter')
})
],
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist/web')
path: path.join(__dirname, '../dist/web'),
globalObject: 'this',
publicPath: ''
},
resolve: {
alias: {
@@ -197,25 +183,31 @@ let webConfig = {
minimizer: [
new TerserPlugin({
extractComments: false,
})
}),
new CssMinimizerPlugin(),
],
},
}
/**
* Adjust webConfig for development settings
*/
if (devMode) {
webConfig.devtool = 'eval-cheap-module-source-map'
}
/**
* Adjust webConfig for production settings
*/
if (!devMode) {
webConfig.devtool = ''
webConfig.plugins.push(
new CopyWebpackPlugin([
{
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
ignore: ['.*']
}
]),
to: path.join(__dirname, '../dist/electron/static'),
globOptions: { ignore: [ '.*' ] }
}]
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 12
@@ -53,7 +53,7 @@ jobs:
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
release: ${{ secrets.force_release == 'true' || startsWith(github.ref, 'refs/tags/v') }}
env:
# macOS notarization API key
API_KEY_ID: ${{ secrets.api_key_id }}
+1 -1
View File
@@ -4,7 +4,7 @@
## 🌍 翻译指南
首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://electronjs.org/docs/api/locales)
首先你要确定一个语言的英文简写作为 **locale**,如 en-US,这个 locale 值请严格参考 [Electron 的 Locales 文档](https://www.electronjs.org/docs/api/app#appgetlocale)
Motrix 的国际化分两部分:
+1 -1
View File
@@ -4,7 +4,7 @@ Before you start contributing, make sure you already understand [GitHub flow](ht
## 🌍 Translation Guide
First you need to determine the English abbreviation of a language as **locale**, such as en-US, this locale value should strictly refer to the [electron's documentation](https://electronjs.org/docs/api/locales).
First you need to determine the English abbreviation of a language as **locale**, such as en-US, this locale value should strictly refer to the [electron's documentation](https://www.electronjs.org/docs/api/app#appgetlocale).
The internationalization of Motrix is divided into two parts:
+52 -16
View File
@@ -1,18 +1,18 @@
# Motrix
<a href="https://motrix.app">
<img src="https://cdn.nlark.com/yuque/0/2018/png/129147/1543735425232-a5d2c99f-d788-43e4-9781-558ff6d21027.png" width="256" alt="App Icon" />
<img src="./static/512x512.png" width="256" alt="App Icon" />
</a>
[English](./README.md) | 简体中文
## 一款全能的下载工具
[![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)
[![GitHub release](https://img.shields.io/github/v/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) ![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
[English](./README.md) | 简体中文
我是个兴趣使然的桌面应用开发者🤓,利用搬砖之余开发了 Motrix。
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链、某盘等资源。它的界面简洁易用,希望大家喜欢 👻。
Motrix 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链等资源。它的界面简洁易用,希望大家喜欢 👻。
✈️ 去 [官网](https://motrix.app/zh-CN) 逛逛 | 📖 查看 [帮助手册](http://motrix.app/support/issues)
@@ -36,15 +36,40 @@ scoop install motrix
macOS 用户可以使用 `brew cask` 安装 Motrix,感谢 [Mitscherlich](https://github.com/Mitscherlich) 的 [PR](https://github.com/Homebrew/homebrew-cask/pull/59494)。
```bash
brew update && brew cask install motrix
brew update && brew install --cask motrix
```
### Linux
你可以下载 AppImage(适用于所有 Linux 发行版)软件包或 snap 或从源代码构建安装 Motrix
你可以下载 `AppImage` (适用于所有 Linux 发行版)`snap` 来安装 Motrix,更多 Linux 安装包格式请查看 [GitHub/release](https://github.com/agalwood/Motrix/releases)
构建请阅读 **编译打包** 部分。
如果你想自己通过编译源码来安装,请阅读 **编译打包** 部分。
#### AppImage
最新版的 Motrix AppImage 需要自己手动进执行桌面集成。请查看 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 的文档进行操作。
> 桌面集成
> electron-builder v21 之后,桌面集成不再是 AppImage 文件的一部分。
> 推荐使用 [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) 集成 AppImage。
Deepin 20 Beta 用户安装 Motrix 失败的问题,请按照以下方法处理:
打开`终端`,黏贴运行如下命令之后再次安装 Motrix。
```bash
sudo apt --fix-broken install
```
#### Snap
Motrix 已经上架 [Snapcraft](https://snapcraft.io/motrix) Ubuntu 用户推荐从 Snap 商店下载。
v1.5.10 提示
系统托盘可能无法正常显示指示器,导致退出应用程序不方便。
请取消勾选 偏好设置——基本设置——隐藏应用程序菜单(仅限Windows和Linux),点击保存并应用。然后点击 "文件 "菜单中的 "退出",退出应用程序。
请更新到 v1.5.12 及以上版本,可以使用键盘组合快捷键 <kbd>Ctrl</kbd> + <kbd>q</kbd> 快速退出应用。
#### AUR
对于 Arch Linux 用户,可以使用 [aur](https://aur.archlinux.org/packages/motrix/) 安装 Motrix,感谢维护者 [weearc](https://github.com/weearc)。
运行以下命令进行安装:
@@ -60,7 +85,8 @@ Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能
- 🕹 简洁明了的图形操作界面
- 🦄 支持BT和磁力链任务
- ☑️ 支持选择性下载BT部分文件
- 💾 支持下载某盘资源
- 📡 每天自动更新 Tracker 服务器列表
- 🔌 UPnP & NAT-PMP 端口映射
- 🎛 最高支持 10 个任务同时下载
- 🚀 单任务最高支持 64 线程下载
- 🚥 设置上传/下载限速
@@ -68,14 +94,15 @@ Motrix 在 Linux 中首次启动可能需要使用 `sudo` 运行,因为可能
- 🔔 下载完成后通知
- 💻 支持触控栏快捷键 (Mac 专享)
- 🤖 常驻系统托盘,操作更加便捷
- 📟 系统托盘速度仪表显示实时速度 (Mac 专享)
- 🌑 深色模式
- 🗑 移除任务时可同时删除相关文件
- 🌍 国际化,[查看已可选的语言](#-国际化)
- 🎏 ...
- 🛠 更多特性开发中
## 🖥 应用界面
![motrix-screenshot-task-cn.png](https://cdn.nlark.com/yuque/0/2019/png/129147/1550151234585-e513bd4f-e127-402f-accb-1ebbba9b3c41.png)
![motrix-screenshot-task-cn.png](https://cdn.nlark.com/yuque/0/2020/png/129147/1589782239990-fecb9065-19ac-4c35-938b-0be45621ca3a.png)
## ⌨️ 本地开发
@@ -89,12 +116,13 @@ git clone git@github.com:agalwood/Motrix.git
```bash
cd Motrix
npm install
yarn
```
天朝大陆用户建议使用淘宝的 npm 源
```bash
yarn config set registry 'https://registry.npm.taobao.org'
npm config set registry 'https://registry.npm.taobao.org'
export ELECTRON_MIRROR='https://npm.taobao.org/mirrors/electron/'
export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
@@ -104,18 +132,16 @@ export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
`Electron` 下载安装失败的问题,解决方式请参考 https://github.com/electron/electron/issues/8466#issuecomment-571425574
如果喜欢 [Yarn](https://yarnpkg.com/),也可以使用 `yarn` 安装依赖
### 开发模式
```bash
npm run dev
yarn run dev
```
### 编译打包
```bash
npm run build
yarn run build
```
完成之后可以在项目的 `release` 目录看到编译打包好的应用文件
@@ -140,17 +166,27 @@ npm run build
| Key | Name | Status |
|-------|:--------------------|:-------------|
| ar | Arabic | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
| bg | Българският език | ✔️ [@null-none](https://github.com/null-none) |
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| el | Ελληνικά | ✔️ [@Likecinema](https://github.com/Likecinema) |
| en-US | English | ✔️ |
| es | Español | ✔️ [@Chofito](https://github.com/Chofito)|
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
| hu | Hungarian | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
| id | Indonesia | ✔️ [@aarestu](https://github.com/aarestu) |
| it | Italiano | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
| pl | Polski | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ro | Română | ✔️ [@alyn3d](https://github.com/alyn3d) |
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| vi | Tiếng Việt | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
| zh-CN | 简体中文 | ✔️ |
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
+50 -13
View File
@@ -1,12 +1,12 @@
# Motrix
<a href="https://motrix.app">
<img src="https://cdn.nlark.com/yuque/0/2018/png/129147/1543735425232-a5d2c99f-d788-43e4-9781-558ff6d21027.png" width="256" alt="App Icon" />
<img src="./static/512x512.png" width="256" alt="App Icon" />
</a>
## A full-featured download manager
[![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)
[![GitHub release](https://img.shields.io/github/v/release/agalwood/Motrix.svg)](https://github.com/agalwood/Motrix/releases) ![Build/release](https://github.com/agalwood/Motrix/workflows/Build/release/badge.svg) ![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)
English | [简体中文](./README-CN.md)
@@ -36,15 +36,42 @@ scoop install motrix
The macOS users can install Motrix using `brew cask`, thanks to [PR](https://github.com/Homebrew/homebrew-cask/pull/59494) of [Mitscherlich](https://github.com/Mitscherlich).
```bash
brew update && brew cask install motrix
brew update && brew install --cask motrix
```
### Linux
You can download the AppImage (for all Linux distributions) package or snap or just build from source code to install Motrix.
You can download the `AppImage` (for all Linux distributions) or `snap` to install Motrix, see [GitHub/release](https://github.com/agalwood/Motrix/releases) for more Linux installation package formats.
Please read the **Build** section.
If you want to build from source code, please read the **Build** section.
#### AppImage
The latest version of Motrix AppImage requires you to manually perform desktop integration. Please check the documentation of [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) .
> Desktop Integration
> Since electron-builder 21 desktop integration is not a part of produced AppImage file.
> [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) is the recommended way to integrate AppImages.
Deepin 20 Beta users failed to install Motrix, please follow the steps below:
Open the `Terminal`, paste and run the following command to install Motrix again.
```bash
sudo apt --fix-broken install
```
#### Snap
Motrix has been listed on [Snapcraft](https://snapcraft.io/motrix) , Ubuntu users recommend downloading from the Snap Store.
Tips for v1.5.10
The tray may not display the indicator normally, which makes it inconvenient to exit the application.
Please unchecked Preferences--Basic Settings--Hide App Menu (Windows & Linux Only), click Save & Apply. Then click "Exit" in the File menu to exit the application.
Please update to v1.5.12 and above, you can use the keyboard shortcut <kbd>Ctrl</kbd> + <kbd>q</kbd> to quickly exit the application.
#### AUR
For Arch Linux users, Motrix is available in [aur](https://aur.archlinux.org/packages/motrix/), thanks to the maintainer [weearc](https://github.com/weearc).
Run the following command to install:
@@ -60,7 +87,8 @@ Motrix may need to run with `sudo` for the first time in Linux because there is
- 🕹 Simple and clear user interface
- 🦄 Supports BitTorrent & Magnet
- ☑️ BitTorrent selective download
- 💾 Supports downloading BD Net Disk
- 📡 Update tracker list every day automatically
- 🔌 UPnP & NAT-PMP Port Mapping
- 🎛 Up to 10 concurrent download tasks
- 🚀 Supports 64 threads in a single task
- 🚥 Supports speed limit
@@ -68,14 +96,15 @@ Motrix may need to run with `sudo` for the first time in Linux because there is
- 🔔 Download completed Notification
- 💻 Ready for Touch Bar (Mac only)
- 🤖 Resident system tray for quick operation
- 📟 Tray speed meter displays real-time speed (Mac only)
- 🌑 Dark mode
- 🗑 Delete related files when removing tasks (optional)
- 🌍 I18n, [View supported languages](#-internationalization).
- 🎏 ...
- 🛠 More features in development
## 🖥 User Interface
![motrix-screenshot-task-en.png](https://cdn.nlark.com/yuque/0/2019/png/129147/1550151166169-94b4bfb0-746e-42b8-aad7-0b6890f89abb.png)
![motrix-screenshot-task-en.png](https://cdn.nlark.com/yuque/0/2020/png/129147/1589782238501-e7b39166-da58-4152-ae34-65a061cafa48.png)
## ⌨️ Development
@@ -89,25 +118,23 @@ git clone git@github.com:agalwood/Motrix.git
```bash
cd Motrix
npm install
yarn
```
> Error: Electron failed to install correctly, please delete node_modules/electron and try installing again
`Electron` failed to install correctly, please refer to https://github.com/electron/electron/issues/8466#issuecomment-571425574
If you like [Yarn](https://yarnpkg.com/), you can also use `yarn` to install dependencies.
### Dev Mode
```bash
npm run dev
yarn run dev
```
### Build Release
```bash
npm run build
yarn run build
```
After building, the application will be found in the project's `release` directory.
@@ -132,17 +159,27 @@ Translations into versions for other languages are welcome 🧐! Please read the
| Key | Name | Status |
|-------|:--------------------|:-------------|
| ar | Arabic | ✔️ [@hadialqattan](https://github.com/hadialqattan), [@AhmedElTabarani](https://github.com/AhmedElTabarani) |
| bg | Българският език | ✔️ [@null-none](https://github.com/null-none) |
| ca | Català | ✔️ [@marcizhu](https://github.com/marcizhu) |
| de | Deutsch | ✔️ [@Schloemicher](https://github.com/Schloemicher) |
| el | Ελληνικά | ✔️ [@Likecinema](https://github.com/Likecinema) |
| en-US | English | ✔️ |
| es | Español | ✔️ [@Chofito](https://github.com/Chofito)|
| fa | فارسی | ✔️ [@Nima-Ra](https://github.com/Nima-Ra) |
| fr | Français | ✔️ [@gpatarin](https://github.com/gpatarin) |
| hu | Hungarian | ✔️ [@zalnaRs](https://github.com/zalnaRs) |
| id | Indonesia | ✔️ [@aarestu](https://github.com/aarestu) |
| it | Italiano | ✔️ [@blackcat-917](https://github.com/blackcat-917) |
| ja | 日本語 | ✔️ [@hbkrkzk](https://github.com/hbkrkzk) |
| ko | 한국어 | ✔️ [@KOZ39](https://github.com/KOZ39) |
| pl | Polski | ✔️ [@KanarekLife](https://github.com/KanarekLife) |
| pt-BR | Portuguese (Brazil) | ✔️ [@andrenoberto](https://github.com/andrenoberto) |
| ro | Română | ✔️ [@alyn3d](https://github.com/alyn3d) |
| ru | Русский | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| tr | Türkçe | ✔️ [@abdullah](https://github.com/abdullah) |
| uk | Українська | ✔️ [@bladeaweb](https://github.com/bladeaweb) |
| vi | Tiếng Việt | ✔️ [@duythanhvn](https://github.com/duythanhvn) |
| zh-CN | 简体中文 | ✔️ |
| zh-TW | 繁體中文 | ✔️ [@Yukaii](https://github.com/Yukaii) |
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

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

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 62 KiB

+5 -1
View File
@@ -40,17 +40,21 @@ max-tries=5
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
# Send Accept: deflate, gzip request header
http-accept-gzip=true
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Try to download first and last pieces of each file first. This is useful for previewing files.
bt-prioritize-piece=head
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
Binary file not shown.
+5 -1
View File
@@ -40,17 +40,21 @@ max-tries=5
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
# Send Accept: deflate, gzip request header
http-accept-gzip=true
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Try to download first and last pieces of each file first. This is useful for previewing files.
bt-prioritize-piece=head
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
+5 -1
View File
@@ -40,17 +40,21 @@ max-tries=5
min-split-size=1M
# Set user agent for HTTP(S) downloads.
user-agent=Transmission/2.94
# Send Accept: deflate, gzip request header
http-accept-gzip=true
################ BT Task ################
# Enable Local Peer Discovery.
bt-enable-lpd=true
# Requires BitTorrent message payload encryption with arc4.
bt-force-encryption=true
# bt-force-encryption=true
# If true is given, after hash check using --check-integrity option and file is complete, continue to seed file.
bt-hash-check-seed=true
# Specify the maximum number of peers per torrent.
bt-max-peers=255
# Try to download first and last pieces of each file first. This is useful for previewing files.
bt-prioritize-piece=head
# Removes the unselected files when download is completed in BitTorrent.
bt-remove-unselected-file=true
# Seed previously downloaded files without verifying piece hashes.
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/renderer/*"
],
"@shared/*": [
"./src/shared/*"
]
}
},
"exclude": ["node_modules", "dist"]
}
-15801
View File
File diff suppressed because it is too large Load Diff
+79 -68
View File
@@ -1,6 +1,6 @@
{
"name": "Motrix",
"version": "1.5.7",
"version": "1.6.9",
"description": "A full-featured download manager",
"homepage": "https://motrix.app",
"author": {
@@ -92,8 +92,20 @@
},
"mac": {
"target": [
"dmg",
"zip"
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"type": "distribution",
"darkModeSupport": true,
@@ -181,84 +193,83 @@
]
},
"dependencies": {
"@babel/runtime": "^7.9.2",
"@babel/runtime": "^7.14.0",
"@motrix/nat-api": "^0.3.1",
"@panter/vue-i18next": "^0.15.2",
"aria2": "^4.1.0",
"axios": "^0.19.2",
"axios": "^0.21.1",
"bittorrent-peerid": "^1.3.3",
"blob-util": "^2.0.2",
"clipboard-polyfill": "^2.8.6",
"electron-debug": "^3.0.1",
"clipboard-polyfill": "^3.0.3",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.0",
"electron-log": "^4.1.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.1",
"element-ui": "^2.13.1",
"forever-monitor": "1.7.2",
"i18next": "^19.4.4",
"lodash": "^4.17.15",
"nat-api": "^0.1.3",
"node-fetch": "^2.6.0",
"electron-log": "^4.3.5",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.8",
"element-ui": "^2.15.1",
"i18next": "^20.2.2",
"lodash": "^4.17.21",
"node-fetch": "^2.6.1",
"normalize.css": "^8.0.1",
"parse-torrent": "^7.1.2",
"parse-torrent": "^9.1.3",
"randomatic": "^3.1.1",
"svg-innerhtml": "^1.1.0",
"vue": "^2.6.11",
"vue": "^2.6.12",
"vue-electron": "^1.0.6",
"vue-router": "^3.1.6",
"vuex": "^3.3.0",
"vuex-router-sync": "^5.0.0"
"vue-router": "^3.5.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"ws": "^7.4.5"
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/register": "^7.9.0",
"@vue/eslint-config-standard": "^5.1.2",
"ajv": "^6.12.2",
"@babel/core": "^7.14.0",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.14.1",
"@babel/register": "^7.13.16",
"@electron/remote": "^1.1.0",
"@motrix/multispinner": "^0.2.2",
"@vue/eslint-config-standard": "^6.0.0",
"ajv": "^8.2.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.2",
"babel-plugin-component": "^1.1.1",
"cfonts": "^2.8.1",
"chalk": "^4.0.0",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3",
"del": "^5.1.0",
"devtron": "^1.4.0",
"electron": "^8.2.3",
"electron-builder": "^22.6.0",
"electron-builder-notarize": "^1.1.2",
"electron-devtools-installer": "^3.0.0",
"electron-notarize": "^0.3.0",
"electron-osx-sign": "^0.4.15",
"eslint": "^6.8.0",
"cfonts": "^2.9.1",
"chalk": "^4.1.1",
"copy-webpack-plugin": "^8.1.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^2.0.0",
"del": "^6.0.0",
"electron": "^11.4.5",
"electron-builder": "22.10.5",
"electron-builder-notarize": "^1.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.0.0",
"electron-osx-sign": "^0.5.0",
"eslint": "^7.25.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"mini-css-extract-plugin": "0.9.0",
"multispinner": "^0.2.1",
"node-loader": "^0.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.0",
"terser-webpack-plugin": "^2.3.6",
"url-loader": "^4.1.0",
"vue-html-loader": "^1.2.4",
"vue-loader": "^15.9.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.9.0",
"eslint-webpack-plugin": "^2.5.4",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "1.6.0",
"node-loader": "^2.0.0",
"sass": "^1.32.12",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.6",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.36.2",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2"
"webpack-merge": "^5.7.3",
"worker-loader": "^3.0.8"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+2 -2
View File
@@ -23,9 +23,9 @@
</section>
</div>
<!-- Set `__static` path to static files in production -->
<% if (!process.browser) { %>
<% if (!htmlWebpackPlugin.options.isBrowser && !htmlWebpackPlugin.options.isDev) { %>
<script>
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
</script>
<% } %>
+256 -77
View File
@@ -1,13 +1,23 @@
import { EventEmitter } from 'events'
import { app, shell, dialog, ipcMain } from 'electron'
import is from 'electron-is'
import { readFile } from 'fs'
import { readFile, unlink } from 'fs'
import { extname, basename } from 'path'
import { isEmpty } from 'lodash'
import {
APP_RUN_MODE,
AUTO_SYNC_TRACKER_INTERVAL,
AUTO_CHECK_UPDATE_INTERVAL
} from '@shared/constants'
import { checkIsNeedRun } from '@shared/utils'
import {
convertTrackerDataToComma,
fetchBtTrackerFromSource
} from '@shared/utils/tracker'
import logger from './core/Logger'
import ConfigManager from './core/ConfigManager'
import { setupLocaleManager } from '@/ui/Locale'
import { setupLocaleManager } from './ui/Locale'
import Engine from './core/Engine'
import EngineClient from './core/EngineClient'
import UPnPManager from './core/UPnPManager'
@@ -21,9 +31,7 @@ import TouchBarManager from './ui/TouchBarManager'
import TrayManager from './ui/TrayManager'
import DockManager from './ui/DockManager'
import ThemeManager from './ui/ThemeManager'
import { AUTO_SYNC_TRACKER_INTERVAL, AUTO_CHECK_UPDATE_INTERVAL } from '@shared/constants'
import { checkIsNeedRun } from '@shared/utils'
import { convertTrackerDataToComma, fetchBtTrackerFromSource } from '@shared/utils/tracker'
import { getSessionPath } from './utils'
export default class Application extends EventEmitter {
constructor () {
@@ -33,45 +41,34 @@ export default class Application extends EventEmitter {
}
init () {
this.configManager = new ConfigManager()
this.configManager = this.initConfigManager()
this.locale = this.configManager.getLocale()
this.localeManager = setupLocaleManager(this.locale)
this.i18n = this.localeManager.getI18n()
this.menuManager = new MenuManager()
this.menuManager.setup(this.locale)
this.initTouchBarManager()
this.setupApplicationMenu()
this.initWindowManager()
this.engine = new Engine({
systemConfig: this.configManager.getSystemConfig(),
userConfig: this.configManager.getUserConfig()
})
this.initUPnPManager()
this.startEngine()
this.initEngineClient()
this.initUPnPManager()
this.initTouchBarManager()
this.autoSyncTracker()
this.initThemeManager()
this.trayManager = new TrayManager({
theme: this.configManager.getUserConfig('tray-theme')
})
this.initTrayManager()
this.dockManager = new DockManager({
runMode: this.configManager.getUserConfig('run-mode')
})
this.initDockManager()
this.autoLaunchManager = new AutoLaunchManager()
this.energyManager = new EnergyManager()
this.initThemeManager()
this.initUpdaterManager()
this.initProtocolManager()
@@ -81,12 +78,52 @@ export default class Application extends EventEmitter {
this.handleEvents()
this.handleIpcMessages()
this.handleIpcInvokes()
this.emit('application:initialized')
}
initConfigManager () {
this.configListeners = {}
return new ConfigManager()
}
offConfigListeners () {
try {
Object.keys(this.configListeners).forEach((key) => {
this.configListeners[key]()
})
} catch (e) {
logger.warn('[Motrix] offConfigListeners===>', e)
}
this.configListeners = {}
}
setupApplicationMenu () {
this.menuManager = new MenuManager()
this.menuManager.setup(this.locale)
}
adjustMenu () {
if (is.mas()) {
const visibleStates = {
'app.check-for-updates': false,
'task.new-bt-task': false
}
this.menuManager.updateMenuStates(visibleStates, null, null)
this.trayManager.updateMenuStates(visibleStates, null, null)
}
}
startEngine () {
const self = this
try {
this.engine = new Engine({
systemConfig: this.configManager.getSystemConfig(),
userConfig: this.configManager.getUserConfig()
})
this.engine.start()
} catch (err) {
const { message } = err
@@ -105,10 +142,13 @@ export default class Application extends EventEmitter {
async stopEngine () {
try {
await this.engineClient.shutdown({ force: true })
setImmediate(() => {
this.engine.stop()
})
} catch (err) {
logger.warn('[Motrix] shutdown engine fail: ', err.message)
} finally {
this.engine.stop()
// no finally
}
}
@@ -121,15 +161,56 @@ export default class Application extends EventEmitter {
})
}
initTrayManager () {
this.trayManager = new TrayManager({
theme: this.configManager.getUserConfig('tray-theme'),
systemTheme: this.themeManager.getSystemTheme(),
speedometer: this.configManager.getUserConfig('tray-speedometer')
})
this.watchTraySpeedometerEnabledChange()
this.trayManager.on('mouse-down', ({ focused }) => {
this.sendCommandToAll('application:update-tray-focused', { focused })
})
this.trayManager.on('mouse-up', ({ focused }) => {
this.sendCommandToAll('application:update-tray-focused', { focused })
})
this.trayManager.on('drop-files', (files = []) => {
this.handleFile(files[0])
})
this.trayManager.on('drop-text', (text) => {
this.handleProtocol(text)
})
}
watchTraySpeedometerEnabledChange () {
const { userConfig } = this.configManager
const key = 'tray-speedometer'
this.configListeners[key] = userConfig.onDidChange('tray-speedometer', async (newValue, oldValue) => {
logger.info('[Motrix] detected tray speedometer value change event:', newValue, oldValue)
this.trayManager.handleSpeedometerEnableChange(newValue)
})
}
initDockManager () {
this.dockManager = new DockManager({
runMode: this.configManager.getUserConfig('run-mode')
})
}
initUPnPManager () {
this.upnp = new UPnPManager()
this.watchEnableUPnPChange()
this.watchUPnPEnabledChange()
this.watchPortsChange()
this.watchUPnPPortsChange()
const enable = this.configManager.getUserConfig('enable-upnp')
if (!enable) {
const enabled = this.configManager.getUserConfig('enable-upnp')
if (!enabled) {
return
}
@@ -145,7 +226,7 @@ export default class Application extends EventEmitter {
this.upnp.map(dhtPort)
]
try {
await Promise.all(promises)
await Promise.allSettled(promises)
} catch (e) {
logger.warn('[Motrix] start UPnP mapping fail', e)
}
@@ -160,17 +241,18 @@ export default class Application extends EventEmitter {
this.upnp.unmap(dhtPort)
]
try {
await Promise.all(promises)
await Promise.allSettled(promises)
} catch (e) {
logger.warn('[Motrix] stop UPnP mapping fail', e)
}
}
watchPortsChange () {
watchUPnPPortsChange () {
const { systemConfig } = this.configManager
const watchKeys = ['listen-port', 'dht-listen-port']
watchKeys.map((key) => {
this.configManager.systemConfig.onDidChange(key, async (newValue, oldValue) => {
watchKeys.forEach((key) => {
this.configListeners[key] = systemConfig.onDidChange(key, async (newValue, oldValue) => {
logger.info('[Motrix] detected port change event:', key, newValue, oldValue)
const enable = this.configManager.getUserConfig('enable-upnp')
if (!enable) {
@@ -182,7 +264,7 @@ export default class Application extends EventEmitter {
this.upnp.map(newValue)
]
try {
await Promise.all(promises)
await Promise.allSettled(promises)
} catch (e) {
logger.info('[Motrix] change UPnP port mapping failed:', e)
}
@@ -190,13 +272,16 @@ export default class Application extends EventEmitter {
})
}
watchEnableUPnPChange () {
this.configManager.userConfig.onDidChange('enable-upnp', async (newValue, oldValue) => {
watchUPnPEnabledChange () {
const { userConfig } = this.configManager
const key = 'enable-upnp'
this.configListeners[key] = userConfig.onDidChange(key, async (newValue, oldValue) => {
logger.info('[Motrix] detected enable-upnp value change event:', newValue, oldValue)
if (newValue) {
this.startUPnPMapping()
} else {
this.stopUPnPMapping()
await this.stopUPnPMapping()
this.upnp.closeClient()
}
})
}
@@ -207,20 +292,30 @@ export default class Application extends EventEmitter {
await this.stopUPnPMapping()
}
this.upnp.destroy()
this.upnp.closeClient()
}
autoSyncTracker () {
const enable = this.configManager.getUserConfig('auto-sync-tracker')
const lastTime = this.configManager.getUserConfig('last-sync-tracker-time')
const result = checkIsNeedRun(enable, lastTime, AUTO_SYNC_TRACKER_INTERVAL)
logger.info('[Motrix] auto sync tracker checkIsNeedRun:', result)
if (!result) {
return
}
const source = this.configManager.getUserConfig('tracker-source')
if (isEmpty(source)) {
return
}
setTimeout(() => {
const source = this.configManager.getUserConfig('tracker-source')
fetchBtTrackerFromSource(source).then((data) => {
logger.warn('[Motrix] auto sync tracker data:', data)
if (!data || data.length === 0) {
return
}
const tracker = convertTrackerDataToComma(data)
this.savePreference({
system: {
@@ -230,8 +325,19 @@ export default class Application extends EventEmitter {
'last-sync-tracker-time': Date.now()
}
})
}).catch((err) => {
logger.warn('[Motrix] auto sync tracker failed:', err.message)
})
}, 3000)
}, 500)
}
autoResumeTask () {
const enabled = this.configManager.getUserConfig('resume-all-when-app-launched')
if (!enabled) {
return
}
this.engineClient.call('unpauseAll')
}
initWindowManager () {
@@ -242,12 +348,25 @@ export default class Application extends EventEmitter {
this.windowManager.on('window-resized', (data) => {
this.storeWindowState(data)
})
this.windowManager.on('window-moved', (data) => {
this.storeWindowState(data)
})
this.windowManager.on('window-closed', (data) => {
this.storeWindowState(data)
})
this.windowManager.on('enter-full-screen', (window) => {
this.dockManager.show()
})
this.windowManager.on('leave-full-screen', (window) => {
const mode = this.configManager.getUserConfig('run-mode')
if (mode !== APP_RUN_MODE.STANDARD) {
this.dockManager.hide()
}
})
}
storeWindowState (data = {}) {
@@ -291,7 +410,7 @@ export default class Application extends EventEmitter {
hide (page) {
if (page) {
this.windowManager.autoHideWindow(page)
this.windowManager.hideWindow(page)
} else {
this.windowManager.hideAllWindow()
}
@@ -311,9 +430,9 @@ export default class Application extends EventEmitter {
this.energyManager.stopPowerSaveBlocker()
this.trayManager.destroy()
await this.stopEngine()
this.trayManager.destroy()
} catch (err) {
logger.warn('[Motrix] stop error: ', err.message)
}
@@ -349,9 +468,9 @@ export default class Application extends EventEmitter {
initThemeManager () {
this.themeManager = new ThemeManager()
this.themeManager.on('system-theme-changed', (theme) => {
this.trayManager.changeIconTheme(theme)
this.sendCommandToAll('application:system-theme', theme)
this.themeManager.on('system-theme-change', (theme) => {
this.trayManager.handleSystemThemeChange(theme)
this.sendCommandToAll('application:update-system-theme', { theme })
})
}
@@ -359,13 +478,11 @@ export default class Application extends EventEmitter {
if (!is.macOS()) {
return
}
this.touchBarManager = new TouchBarManager()
}
initProtocolManager () {
if (is.dev() || is.mas()) {
return
}
const protocols = this.configManager.getUserConfig('protocols', {})
this.protocolManager = new ProtocolManager({
protocols
@@ -373,10 +490,6 @@ export default class Application extends EventEmitter {
}
handleProtocol (url) {
if (is.dev() || is.mas()) {
return
}
this.show()
this.protocolManager.handle(url)
@@ -393,15 +506,17 @@ export default class Application extends EventEmitter {
this.show()
const fileName = basename(filePath)
const name = basename(filePath)
readFile(filePath, (err, data) => {
if (err) {
logger.warn(`[Motrix] read file error: ${filePath}`, err.message)
return
}
const file = Buffer.from(data).toString('base64')
const args = [fileName, file]
this.sendCommandToAll('application:new-bt-task-with-file', ...args)
const dataURL = Buffer.from(data).toString('base64')
this.sendCommandToAll('application:new-bt-task-with-file', {
name,
dataURL
})
})
}
@@ -410,10 +525,10 @@ export default class Application extends EventEmitter {
return
}
const enable = this.configManager.getUserConfig('auto-check-update')
const enabled = this.configManager.getUserConfig('auto-check-update')
const lastTime = this.configManager.getUserConfig('last-check-update-time')
this.updateManager = new UpdateManager({
autoCheck: checkIsNeedRun(enable, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
autoCheck: checkIsNeedRun(enabled, lastTime, AUTO_CHECK_UPDATE_INTERVAL)
})
this.handleUpdaterEvents()
}
@@ -452,12 +567,27 @@ export default class Application extends EventEmitter {
})
}
relaunch () {
this.stop()
async relaunch () {
await this.stop()
app.relaunch()
app.exit()
}
async resetSession () {
await this.stopEngine()
app.clearRecentDocuments()
const sessionPath = this.configManager.getUserConfig('session-path') || getSessionPath()
setTimeout(() => {
unlink(sessionPath, function (err) {
logger.info('[Motrix] Removed the download seesion file:', err)
})
this.engine.start()
}, 3000)
}
savePreference (config = {}) {
logger.info('[Motrix] save preference:', config)
const { system, user } = config
@@ -476,6 +606,10 @@ export default class Application extends EventEmitter {
handleCommands () {
this.on('application:save-preference', this.savePreference)
this.on('application:update-tray', (tray) => {
this.trayManager.updateTrayByImage(tray)
})
this.on('application:relaunch', () => {
this.relaunch()
})
@@ -496,15 +630,18 @@ export default class Application extends EventEmitter {
}
})
this.on('application:show', (page) => {
this.on('application:show', ({ page }) => {
this.show(page)
})
this.on('application:hide', (page) => {
this.on('application:hide', ({ page }) => {
this.hide(page)
})
this.on('application:reset-session', () => this.resetSession())
this.on('application:reset', () => {
this.offConfigListeners()
this.configManager.reset()
this.relaunch()
})
@@ -515,14 +652,14 @@ export default class Application extends EventEmitter {
this.on('application:change-theme', (theme) => {
this.themeManager.updateAppAppearance(theme)
this.sendCommandToAll('application:theme', theme)
this.sendCommandToAll('application:update-theme', { theme })
})
this.on('application:change-locale', (locale) => {
this.localeManager.changeLanguageByLocale(locale)
.then(() => {
this.menuManager.setup(locale)
this.trayManager.setup(locale)
this.menuManager.handleLocaleChange(locale)
this.trayManager.handleLocaleChange(locale)
})
})
@@ -580,34 +717,57 @@ export default class Application extends EventEmitter {
this.protocolManager.setup(protocols)
})
this.on('application:open-external', (url) => {
this.openExternal(url)
})
this.on('help:official-website', () => {
const url = 'https://motrix.app/'
shell.openExternal(url)
this.openExternal(url)
})
this.on('help:manual', () => {
const url = 'https://motrix.app/manual'
shell.openExternal(url)
this.openExternal(url)
})
this.on('help:release-notes', () => {
const url = 'https://motrix.app/release'
shell.openExternal(url)
this.openExternal(url)
})
this.on('help:report-problem', () => {
const url = 'https://motrix.app/report'
shell.openExternal(url)
this.openExternal(url)
})
}
openExternal (url) {
if (!url) {
return
}
shell.openExternal(url)
}
handleConfigChange (configName) {
this.sendCommandToAll('application:update-preference-config', { configName })
}
handleEvents () {
// this.configManager.systemConfig.onDidAnyChange(() => {
// this.engineClient.changeGlobalOption(this.configManager.getSystemConfig())
// })
this.once('application:initialized', () => {
this.autoSyncTracker()
this.autoResumeTask()
this.adjustMenu()
})
this.configManager.userConfig.onDidAnyChange(() => this.handleConfigChange('user'))
this.configManager.systemConfig.onDidAnyChange(() => this.handleConfigChange('system'))
this.on('download-status-change', (downloading) => {
this.trayManager.updateTrayByStatus(downloading)
this.trayManager.handleDownloadStatusChange(downloading)
if (downloading) {
this.energyManager.startPowerSaveBlocker()
} else {
@@ -615,12 +775,18 @@ export default class Application extends EventEmitter {
}
})
this.on('download-speed-change', (speed) => {
this.dockManager.setBadge(speed)
this.on('speed-change', (speed) => {
this.dockManager.handleSpeedChange(speed)
this.trayManager.handleSpeedChange(speed)
})
this.on('task-download-complete', (task, path) => {
this.dockManager.openDock(path)
if (is.linux()) {
return
}
app.addRecentDocument(path)
})
}
@@ -635,4 +801,17 @@ export default class Application extends EventEmitter {
this.emit(eventName, ...args)
})
}
handleIpcInvokes () {
ipcMain.handle('get-app-config', async () => {
const systemConfig = this.configManager.getSystemConfig()
const userConfig = this.configManager.getUserConfig()
const result = {
...systemConfig,
...userConfig
}
return result
})
}
}
+1 -1
View File
@@ -12,6 +12,6 @@ export default {
transparent: !is.windows()
},
bindCloseToHide: true,
url: is.dev() ? 'http://localhost:9080' : `file://${__dirname}/index.html`
url: is.dev() ? 'http://localhost:9080' : require('path').join('file://', __dirname, '/index.html')
}
}
+6 -7
View File
@@ -1,19 +1,18 @@
import { app } from 'electron'
import { LOGIN_SETTING_OPTIONS } from '@shared/constants'
export default class AutoLaunchManager {
enable () {
return new Promise((resolve, reject) => {
const enabled = app.getLoginItemSettings().openAtLogin
const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
if (enabled) {
resolve()
}
app.setLoginItemSettings({
openAtLogin: true,
// For Windows
args: [
'--opened-at-login=1'
]
...LOGIN_SETTING_OPTIONS,
openAtLogin: true
})
resolve()
})
@@ -28,7 +27,7 @@ export default class AutoLaunchManager {
isEnabled () {
return new Promise((resolve, reject) => {
const enabled = app.getLoginItemSettings().openAtLogin
const enabled = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
resolve(enabled)
})
}
+16 -9
View File
@@ -13,9 +13,10 @@ import {
APP_RUN_MODE,
APP_THEME,
EMPTY_STRING,
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL,
IP_VERSION
IP_VERSION,
LOGIN_SETTING_OPTIONS,
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
NGOSANG_TRACKERS_BEST_URL_CDN
} from '@shared/constants'
import { separateConfig } from '@shared/utils'
@@ -49,6 +50,9 @@ export default class ConfigManager {
'all-proxy': EMPTY_STRING,
'allow-overwrite': false,
'auto-file-renaming': true,
'bt-exclude-tracker': EMPTY_STRING,
'bt-load-saved-metadata': true,
'bt-save-metadata': true,
'bt-tracker': EMPTY_STRING,
'continue': true,
'dht-file-path': getDhtPath(IP_VERSION.V4),
@@ -68,7 +72,7 @@ export default class ConfigManager {
'rpc-secret': EMPTY_STRING,
'seed-ratio': 1,
'seed-time': 60,
'split': 128,
'split': getMaxConnectionPerServer(),
'user-agent': 'Transmission/2.94'
}
/* eslint-enable quote-props */
@@ -96,12 +100,14 @@ export default class ConfigManager {
'enable-upnp': true,
'engine-max-connection-per-server': getMaxConnectionPerServer(),
'hide-app-menu': is.windows() || is.linux(),
'keep-seeding': false,
'keep-window-state': false,
'last-check-update-time': 0,
'last-sync-tracker-time': 0,
'locale': app.getLocale(),
'log-path': getLogPath(),
'new-task-show-downloading': true,
'no-confirm-before-delete-task': false,
'open-at-login': false,
'protocols': { 'magnet': true, 'thunder': false },
'resume-all-when-app-launched': false,
@@ -110,10 +116,11 @@ export default class ConfigManager {
'task-notification': true,
'theme': APP_THEME.AUTO,
'tracker-source': [
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
NGOSANG_TRACKERS_BEST_URL_CDN
],
'tray-theme': APP_THEME.AUTO,
'tray-speedometer': is.macOS(),
'update-channel': 'latest',
'use-proxy': false,
'window-state': {}
@@ -138,15 +145,15 @@ export default class ConfigManager {
fixUserConfig () {
// Fix the value of open-at-login when the user delete
// the Motrix self-starting item through startup management.
const openAtLogin = app.getLoginItemSettings().openAtLogin
const openAtLogin = app.getLoginItemSettings(LOGIN_SETTING_OPTIONS).openAtLogin
if (this.getUserConfig('open-at-login') !== openAtLogin) {
this.setUserConfig('open-at-login', openAtLogin)
}
if (this.getUserConfig('tracker-source').length === 0) {
this.setUserConfig('tracker-source', [
NGOSANG_TRACKERS_BEST_IP_URL,
NGOSANG_TRACKERS_BEST_URL
NGOSANG_TRACKERS_BEST_IP_URL_CDN,
NGOSANG_TRACKERS_BEST_URL_CDN
])
}
}
+93 -79
View File
@@ -1,18 +1,22 @@
import { app } from 'electron'
import is from 'electron-is'
import { existsSync } from 'fs'
import { existsSync, writeFile, unlink } from 'fs'
import { resolve, join } from 'path'
import forever from 'forever-monitor'
import { spawn } from 'child_process'
import logger from './Logger'
import { getI18n } from '@/ui/Locale'
import { getI18n } from '../ui/Locale'
import {
getEngineBin,
getEnginePidPath,
getSessionPath,
transformConfig
} from '../utils/index'
const { platform } = process
export default class Engine {
// ChildProcess | null
static instance = null
constructor (options = {}) {
@@ -21,84 +25,116 @@ export default class Engine {
this.i18n = getI18n()
this.systemConfig = options.systemConfig
this.userConfig = options.userConfig
this.basePath = this.getBasePath()
}
getStartSh () {
const { platform } = process
let basePath = resolve(app.getAppPath(), '..')
start () {
const pidPath = getEnginePidPath()
logger.info('[Motrix] Engie pid path:', pidPath)
if (is.dev()) {
basePath = resolve(__dirname, `../../../extra/${platform}`)
if (this.instance) {
return
}
const binPath = this.getBinPath()
const args = this.getStartArgs()
this.instance = spawn(binPath, args, {
windowsHide: false,
stdio: is.dev() ? 'pipe' : 'ignore'
})
const pid = this.instance.pid.toString()
this.writePidFile(pidPath, pid)
this.instance.once('close', function () {
try {
unlink(pidPath, function (err) {
if (err) {
logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
}
})
} catch (err) {
logger.warn(`[Motrix] Unlink engine process pid file failed: ${err}`)
}
})
if (is.dev()) {
this.instance.stdout.on('data', function (data) {
logger.log('[Motrix] engine stdout===>', data.toString())
})
this.instance.stderr.on('data', function (data) {
logger.log('[Motrix] engine stderr===>', data.toString())
})
}
}
stop () {
if (this.instance) {
this.instance.kill()
this.instance = null
}
}
writePidFile (pidPath, pid) {
writeFile(pidPath, pid, (err) => {
if (err) {
logger.error(`[Motrix] Write engine process pid failed: ${err}`)
}
})
}
getBinPath () {
const binName = getEngineBin(platform)
if (!binName) {
throw new Error(this.i18n.t('app.engine-damaged-message'))
}
const binPath = join(basePath, `/engine/${binName}`)
const binIsExist = existsSync(binPath)
const result = join(this.basePath, `/engine/${binName}`)
const binIsExist = existsSync(result)
if (!binIsExist) {
logger.error('[Motrix] engine bin is not exist:', binPath)
logger.error('[Motrix] engine bin is not exist:', result)
throw new Error(this.i18n.t('app.engine-missing-message'))
}
const confPath = join(basePath, '/engine/aria2.conf')
const sessionPath = this.userConfig['session-path'] || getSessionPath()
const sessionIsExist = existsSync(sessionPath)
let result = [`${binPath}`, `--conf-path=${confPath}`, `--save-session=${sessionPath}`]
if (sessionIsExist) {
result = [...result, `--input-file=${sessionPath}`]
}
const extraConfig = transformConfig(this.systemConfig)
result = [...result, ...extraConfig]
return result
}
start () {
const sh = this.getStartSh()
logger.info('[Motrix] Engine start sh:', sh)
this.instance = forever.start(sh, {
max: is.dev() ? 1 : 100,
parser: function (command, args) {
return {
command: command,
args: args
}
},
silent: !is.dev()
})
getBasePath () {
let result = resolve(app.getAppPath(), '..')
const { child } = this.instance
logger.info('[Motrix] Engine pid:', child.pid)
if (is.dev()) {
result = resolve(__dirname, `../../../extra/${platform}`)
}
this.instance.on('error', (err) => {
logger.info(`[Motrix] Engine error: ${err}`)
})
return result
}
this.instance.on('start', function (process, data) {
logger.info('[Motrix] Engine started')
})
getStartArgs () {
const confPath = join(this.basePath, '/engine/aria2.conf')
this.instance.on('stop', function (process) {
logger.info('[Motrix] Engine stopped')
})
const sessionPath = this.userConfig['session-path'] || getSessionPath()
const sessionIsExist = existsSync(sessionPath)
// this.instance.on('restart', function (forever) {
// logger.info(`[Motrix] Engine exit:`)
// })
let result = [`--conf-path=${confPath}`, `--save-session=${sessionPath}`]
if (sessionIsExist) {
result = [...result, `--input-file=${sessionPath}`]
}
// this.instance.on('exit:code', function (code) {
// logger.info(`[Motrix] Engine exit: ${code}`)
// })
const extraConfig = {
...this.systemConfig
}
const keepSeeding = this.userConfig['keep-seeding']
const seedRatio = this.systemConfig['seed-ratio']
if (keepSeeding || seedRatio === 0) {
extraConfig['seed-ratio'] = 0
delete extraConfig['seed-time']
}
console.log('extraConfig===>', extraConfig)
// this.instance.on('stderr', (data) => {
// logger.info(`[Motrix] Engine stderr: ${data}`)
// })
const extra = transformConfig(extraConfig)
result = [...result, ...extra]
return result
}
isRunning (pid) {
@@ -109,28 +145,6 @@ export default class Engine {
}
}
stop () {
const { pid } = this.instance.child
try {
logger.info('[Motrix] Engine stopping')
this.instance.stop()
} catch (err) {
logger.error('[Motrix] Engine stop fail:', err.message)
this.forceStop(pid)
} finally {
}
}
forceStop (pid) {
try {
if (pid && this.isRunning(pid)) {
process.kill(pid)
}
} catch (err) {
logger.warn('[Motrix] Engine force stop fail:', err)
}
}
restart () {
this.stop()
this.start()
+1 -1
View File
@@ -1,6 +1,6 @@
'use strict'
import Aria2 from 'aria2'
import { Aria2 } from '@shared/aria2'
import logger from './Logger'
import {
+19 -10
View File
@@ -1,6 +1,7 @@
import { EventEmitter } from 'events'
import { app } from 'electron'
import is from 'electron-is'
import { parse } from 'querystring'
import logger from './Logger'
import protocolMap from '../configs/protocol'
@@ -28,6 +29,10 @@ export default class ProtocolManager extends EventEmitter {
}
setup (protocols) {
if (is.dev() || is.mas()) {
return
}
Object.keys(protocols).forEach((protocol) => {
const enabled = protocols[protocol]
if (enabled) {
@@ -44,10 +49,13 @@ export default class ProtocolManager extends EventEmitter {
logger.info(`[Motrix] protocol url: ${url}`)
if (
url.toLowerCase().startsWith('ftp:') ||
url.toLowerCase().startsWith('http:') ||
url.toLowerCase().startsWith('https:') ||
url.toLowerCase().startsWith('magnet:') ||
url.toLowerCase().startsWith('thunder:')
) {
return this.handleMagnetAndThunderProtocol(url)
return this.handleResourceProtocol(url)
}
if (
@@ -58,17 +66,20 @@ export default class ProtocolManager extends EventEmitter {
}
}
handleMagnetAndThunderProtocol (url) {
handleResourceProtocol (url) {
if (!url) {
return
}
global.application.sendCommandToAll('application:new-task', ADD_TASK_TYPE.URI, url)
global.application.sendCommandToAll('application:new-task', {
type: ADD_TASK_TYPE.URI,
uri: url
})
}
handleMoProtocol (url) {
const parsed = new URL(url)
const { host } = parsed
const { host, search } = parsed
logger.info('[Motrix] protocol parsed:', parsed, host)
const command = protocolMap[host]
@@ -76,10 +87,8 @@ export default class ProtocolManager extends EventEmitter {
return
}
// @TODO 没想明白怎么传参数好
// 如果按顺序传递,那 url 的 query string 就要求有序的了
// const query = queryString.parse(parsed.query)
const args = []
global.application.sendCommandToAll(command, ...args)
const query = search.startsWith('?') ? search.replace('?', '') : search
const args = parse(query)
global.application.sendCommandToAll(command, args)
}
}
+40 -25
View File
@@ -1,4 +1,4 @@
import NatAPI from 'nat-api'
import NatAPI from '@motrix/nat-api'
import logger from './Logger'
@@ -17,7 +17,9 @@ export default class UPnPManager {
return
}
client = new NatAPI()
client = new NatAPI({
autoUpdate: true
})
}
map (port) {
@@ -30,17 +32,21 @@ export default class UPnPManager {
return
}
client.map(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err)
reject(err.message)
return
}
try {
client.map(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager map ${port} failed, error: `, err)
reject(err.message)
return
}
mappingStatus[port] = true
logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
resolve()
})
mappingStatus[port] = true
logger.info(`[Motrix] UPnPManager port ${port} mapping succeeded`)
resolve()
})
} catch (err) {
reject(err.message)
}
})
}
@@ -59,26 +65,35 @@ export default class UPnPManager {
return
}
client.unmap(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
reject(err.message)
return
}
try {
client.unmap(port, (err) => {
if (err) {
logger.warn(`[Motrix] UPnPManager unmap ${port} failed, error: `, err)
reject(err.message)
return
}
logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
mappingStatus[port] = false
resolve()
})
logger.info(`[Motrix] UPnPManager port ${port} unmapping succeeded`)
mappingStatus[port] = false
resolve()
})
} catch (err) {
reject(err.message)
}
})
}
destroy () {
closeClient () {
if (!client) {
return
}
client.destroy()
client = null
try {
client.destroy(() => {
client = null
})
} catch (err) {
logger.warn('[Motrix] close UPnP client fail', err)
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import is from 'electron-is'
import { autoUpdater } from 'electron-updater'
import logger from './Logger'
import { getI18n } from '@/ui/Locale'
import { getI18n } from '../ui/Locale'
if (is.dev()) {
autoUpdater.updateConfigPath = resolve(__dirname, '../../../app-update.yml')
+8
View File
@@ -1,8 +1,16 @@
import { app } from 'electron'
import is from 'electron-is'
import { initialize } from '@electron/remote/main'
import Launcher from './Launcher'
/**
* initialize the main-process side of the remote module
*/
initialize()
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
if (process.env.NODE_ENV !== 'development') {
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}
+5 -5
View File
@@ -3,7 +3,7 @@
{
"id": "menu.app",
"submenu": [
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "app.preferences", "command": "application:preferences" },
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
@@ -11,15 +11,15 @@
{ "id": "app.hide-others", "role": "hideothers" },
{ "id": "app.unhide", "role": "unhide" },
{ "type": "separator" },
{ "id": "app.quit", "command": "application:quit" }
{ "id": "app.quit", "role": "quit" }
]
},
{
"id": "menu.task",
"submenu": [
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "task.pause-task", "command": "application:pause-task" },
{ "id": "task.resume-task", "command": "application:resume-task" },
+6 -6
View File
@@ -3,21 +3,21 @@
{
"id": "menu.file",
"submenu": [
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "app.preferences", "command": "application:preferences" },
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
{ "type": "separator" },
{ "id": "app.quit", "command": "application:quit" }
{ "id": "app.quit", "role": "quit" }
]
},
{
"id": "menu.task",
"submenu": [
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "task.pause-task", "command": "application:pause-task" },
{ "id": "task.resume-task", "command": "application:resume-task" },
+5 -5
View File
@@ -1,6 +1,6 @@
[
{
"type": "button", "icon": "new-task", "id": "task.new-task", "command": "application:new-task", "command-arg": "uri", "command-after": "application:show,index"
"type": "button", "icon": "new-task", "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index"
},
{
"type": "spacer", "size": "small"
@@ -10,13 +10,13 @@
"id": "task.task-list",
"items": [
{
"type": "button", "icon": "task-active", "command": "application:task-list", "command-arg": "active"
"type": "button", "icon": "task-active", "command": "application:task-list", "command-arg": { "status": "active" }
},
{
"type": "button", "icon": "task-waiting", "command": "application:task-list", "command-arg": "waiting"
"type": "button", "icon": "task-waiting", "command": "application:task-list", "command-arg": { "status": "waiting" }
},
{
"type": "button", "icon": "task-stopped", "command": "application:task-list", "command-arg": "stopped"
"type": "button", "icon": "task-stopped", "command": "application:task-list", "command-arg": { "status": "stopped" }
}
]
},
@@ -30,6 +30,6 @@
"type": "spacer", "size": "small"
},
{
"type": "button", "icon": "about", "id": "app.about", "command": "application:about", "command-before": "application:show,index"
"type": "button", "icon": "about", "id": "app.about", "command": "application:about", "command-before": "application:show?page=index"
}
]
+5 -5
View File
@@ -1,12 +1,12 @@
[
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": { "type": "torrent" }, "command-after": "application:show?page=index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
{ "id": "help.manual", "command": "help:manual" },
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
{ "type": "separator" },
{ "id": "app.preferences", "command": "application:preferences", "command-before": "application:show,index" },
{ "id": "app.preferences", "command": "application:preferences", "command-before": "application:show?page=index" },
{ "id": "app.quit", "command": "application:quit" }
]
+6 -6
View File
@@ -3,21 +3,21 @@
{
"id": "menu.file",
"submenu": [
{ "id": "app.about", "command": "application:about", "command-before": "application:show,index" },
{ "id": "app.about", "command": "application:about", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "app.preferences", "command": "application:preferences" },
{ "id": "app.check-for-updates", "command": "application:check-for-updates" },
{ "id": "app.show", "command": "application:show", "command-arg": "index" },
{ "id": "app.show", "command": "application:show", "command-arg": { "page": "index" } },
{ "type": "separator" },
{ "id": "app.quit", "command": "application:quit" }
{ "id": "app.quit", "role": "quit" }
]
},
{
"id": "menu.task",
"submenu": [
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show,index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-arg": "torrent", "command-after": "application:show,index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show,index" },
{ "id": "task.new-task", "command": "application:new-task", "command-after": "application:show?page=index" },
{ "id": "task.new-bt-task", "command": "application:new-bt-task", "command-after": "application:show?page=index" },
{ "id": "task.open-file", "command": "application:open-file", "command-before": "application:show?page=index" },
{ "type": "separator" },
{ "id": "task.pause-task", "command": "application:pause-task" },
{ "id": "task.resume-task", "command": "application:resume-task" },
+38 -12
View File
@@ -2,6 +2,8 @@ import is from 'electron-is'
import { EventEmitter } from 'events'
import { app } from 'electron'
import { bytesToSize } from '@shared/utils'
import {
APP_RUN_MODE
} from '@shared/constants'
@@ -18,19 +20,43 @@ export default class DockManager extends EventEmitter {
}
}
show = isMac ? () => {
return app.dock.show()
} : () => {}
show = isMac
? () => {
if (app.dock.isVisible()) {
return
}
hide = isMac ? () => {
app.dock.hide()
} : () => {}
return app.dock.show()
}
: () => {}
setBadge = isMac ? (text) => {
app.dock.setBadge(text)
} : (text) => {}
hide = isMac
? () => {
if (!app.dock.isVisible()) {
return
}
openDock = isMac ? (path) => {
app.dock.downloadFinished(path)
} : (path) => {}
app.dock.hide()
}
: () => {}
setBadge = isMac
? (text) => {
app.dock.setBadge(text)
}
: (text) => {}
handleSpeedChange = isMac
? (speed) => {
const { downloadSpeed } = speed
const text = downloadSpeed > 0 ? `${bytesToSize(downloadSpeed)}/s` : ''
this.setBadge(text)
}
: (text) => {}
openDock = isMac
? (path) => {
app.dock.downloadFinished(path)
}
: (path) => {}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
updateStates
} from '../utils/menu'
import keymap from '@shared/keymap'
import { getI18n } from '@/ui/Locale'
import { getI18n } from '../ui/Locale'
export default class MenuManager extends EventEmitter {
constructor (options) {
@@ -47,7 +47,7 @@ export default class MenuManager extends EventEmitter {
this.items = flattenMenuItems(menu)
}
rebuild () {
handleLocaleChange (locale) {
this.setup()
}
+15 -10
View File
@@ -1,40 +1,45 @@
import { EventEmitter } from 'events'
import { nativeTheme, systemPreferences } from 'electron'
import is from 'electron-is'
import { APP_THEME } from '@shared/constants'
import { getSystemTheme } from '../utils'
export default class ThemeManager extends EventEmitter {
constructor (options = {}) {
super()
this.options = options
this.init()
}
init () {
this.systemTheme = getSystemTheme()
this.handleEvents()
}
getSystemTheme () {
let result = APP_THEME.LIGHT
if (!is.macOS()) {
return result
}
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
return this.systemTheme
}
handleEvents () {
if (!is.macOS()) {
return
}
nativeTheme.on('updated', () => {
const theme = this.getSystemTheme()
this.updateAppAppearance(theme)
this.emit('system-theme-changed', theme)
const theme = getSystemTheme()
this.systemTheme = theme
console.log('nativeTheme updated===>', theme)
this.emit('system-theme-change', theme)
})
}
/**
* deprecated
* @see https://www.electronjs.org/docs/all#systempreferencessetapplevelappearanceappearance-macos-deprecated
*/
updateAppAppearance (theme) {
if (!is.macOS() || theme !== APP_THEME.LIGHT || theme !== APP_THEME.DARK) {
return
+255 -48
View File
@@ -1,54 +1,145 @@
import { EventEmitter } from 'events'
import { join } from 'path'
import { Tray, Menu, nativeTheme } from 'electron'
import { Tray, Menu, nativeImage } from 'electron'
import is from 'electron-is'
import { APP_THEME } from '@shared/constants'
import { getInverseTheme, getSystemMajorVersion } from '@shared/utils'
import { getI18n } from './Locale'
import {
translateTemplate,
flattenMenuItems,
updateStates
} from '../utils/menu'
import { getI18n } from '@/ui/Locale'
import { APP_THEME } from '@shared/constants'
import { convertArrayBufferToBuffer } from '../utils/index'
// import logger from '../core/Logger'
let tray = null
const { platform } = process
export default class TrayManager extends EventEmitter {
constructor (options = {}) {
super()
this.options = options
this.theme = options.theme || APP_THEME.AUTO
this.systemTheme = options.systemTheme
this.inverseSystemTheme = getInverseTheme(this.systemTheme)
this.bigSur = platform === 'darwin' && getSystemMajorVersion() >= 20
this.speedometer = options.speedometer
this.i18n = getI18n()
this.menu = null
this.cache = {}
this.uploadSpeed = 0
this.downloadSpeed = 0
this.status = false
this.focused = false
this.load()
this.init()
this.setup()
}
init () {
this.loadTemplate()
this.loadImages()
this.initTray()
this.setupMenu()
this.handleEvents()
}
load () {
loadTemplate () {
this.template = require('../menus/tray.json')
}
let theme = APP_THEME.LIGHT
loadImages () {
switch (platform) {
case 'darwin':
this.loadImagesForMacOS()
break
case 'win32':
this.loadImagesForWindows()
break
case 'linux':
this.loadImagesForLinux()
break
if (is.windows()) {
theme = 'colorful'
} else if (is.macOS()) {
theme = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
} else if (is.linux()) {
theme = (this.theme === APP_THEME.AUTO) ? APP_THEME.DARK : this.theme
default:
this.loadImagesForDefault()
break
}
}
loadImagesForMacOS () {
if (this.bigSur) {
const {
systemTheme,
inverseSystemTheme
} = this
this.normalIcon = this.getFromCacheOrCreateImage(`mo-tray-${systemTheme}-normal.png`)
this.activeIcon = this.getFromCacheOrCreateImage(`mo-tray-${systemTheme}-active.png`)
// if (systemTheme === APP_THEME.DARK) {
// this.inverseNormalIcon = this.normalIcon
// this.inverseActiveIcon = this.activeIcon
// } else {
this.inverseNormalIcon = this.getFromCacheOrCreateImage(`mo-tray-${inverseSystemTheme}-normal.png`)
this.inverseActiveIcon = this.getFromCacheOrCreateImage(`mo-tray-${inverseSystemTheme}-active.png`)
// }
} else {
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
}
}
loadImagesForWindows () {
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-normal.png')
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-colorful-active.png')
}
loadImagesForLinux () {
const { theme } = this
if (theme === APP_THEME.AUTO) {
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-dark-normal.png')
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-dark-active.png')
} else {
this.normalIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-normal.png`)
this.activeIcon = this.getFromCacheOrCreateImage(`mo-tray-${theme}-active.png`)
}
}
loadImagesForDefault () {
this.normalIcon = this.getFromCacheOrCreateImage('mo-tray-light-normal.png')
this.activeIcon = this.getFromCacheOrCreateImage('mo-tray-light-active.png')
}
getFromCacheOrCreateImage (key) {
let file = this.getCache(key)
if (file) {
return file
}
this.setIcons(theme)
file = nativeImage.createFromPath(join(__static, `./${key}`))
file.setTemplateImage(this.bigSur)
this.setCache(key, file)
return file
}
setIcons (theme) {
this.normalIcon = join(__static, `./mo-tray-${theme}-normal.png`)
this.activeIcon = join(__static, `./mo-tray-${theme}-active.png`)
getCache (key) {
return this.cache[key]
}
build () {
setCache (key, value) {
this.cache[key] = value
}
buildMenu () {
const keystrokesByCommand = {}
for (const item in this.keymap) {
keystrokesByCommand[this.keymap[item]] = item
@@ -61,73 +152,129 @@ export default class TrayManager extends EventEmitter {
this.items = flattenMenuItems(this.menu)
}
setup () {
this.build()
setupMenu () {
this.buildMenu()
/**
* Linux requires setContextMenu to be called
* in order for the context menu to populate correctly
*/
if (process.platform === 'linux') {
tray.setContextMenu(this.menu)
}
this.updateContextMenu()
}
init () {
tray = new Tray(this.normalIcon)
initTray () {
const { icon } = this.getIcons()
tray = new Tray(icon)
// tray.setPressedImage(inverseIcon)
tray.setToolTip('Motrix')
}
handleEvents () {
// All OS
tray.on('click', this.handleTrayClick)
tray.on('double-click', this.handleTrayDbClick)
tray.on('right-click', this.handleTrayRightClick)
tray.on('drop-files', this.handleTrayDropFile)
// macOS, Windows
// tray.on('double-click', this.handleTrayDbClick)
tray.on('right-click', this.handleTrayRightClick)
tray.on('mouse-down', this.handleTrayMouseDown)
tray.on('mouse-up', this.handleTrayMouseUp)
// macOS only
tray.setIgnoreDoubleClickEvents(true)
tray.on('drop-files', this.handleTrayDropFiles)
tray.on('drop-text', this.handleTrayDropText)
}
handleTrayClick = (event) => {
event.preventDefault()
global.application.toggle()
}
handleTrayDbClick = (event) => {
event.preventDefault()
global.application.show()
}
handleTrayRightClick = (event) => {
event.preventDefault()
tray.popUpContextMenu(this.menu)
}
handleTrayDropFile = (event, files) => {
global.application.show()
global.application.handleFile(files[0])
handleTrayMouseDown = (event) => {
this.focused = true
this.emit('mouse-down', {
focused: true,
theme: this.inverseSystemTheme
})
this.renderTray()
}
updateTrayByStatus (status) {
this.status = status
this.updateTray()
handleTrayMouseUp = (event) => {
this.focused = false
this.emit('mouse-up', {
focused: false,
theme: this.theme
})
this.renderTray()
}
updateTray () {
const icon = this.status ? this.activeIcon : this.normalIcon
tray.setImage(icon)
handleTrayDropFiles = (event, files) => {
this.emit('drop-files', files)
}
changeIconTheme (theme = APP_THEME.LIGHT) {
if (!is.macOS()) {
handleTrayDropText = (event, text) => {
this.emit('drop-text', text)
}
toggleSpeedometer (enabled) {
this.speedometer = enabled
}
async renderTray () {
if (this.speedometer) {
return
}
this.setIcons(theme)
const { icon } = this.getIcons()
this.updateTray()
tray.setImage(icon)
// tray.setPressedImage(inverseIcon)
this.updateContextMenu()
}
getIcons () {
if (this.bigSur) {
return { icon: this.normalIcon }
}
const { focused, status, systemTheme } = this
const icon = status ? this.activeIcon : this.normalIcon
if (systemTheme === APP_THEME.DARK) {
return {
icon
}
}
const inverseIcon = status ? this.inverseActiveIcon : this.inverseNormalIcon
return {
icon: focused ? inverseIcon : icon
// inverseIcon: focused ? icon : inverseIcon
}
}
updateContextMenu () {
/**
* Linux requires setContextMenu to be called
* in order for the context menu to populate correctly
*/
if (process.platform !== 'linux') {
return
}
tray.setContextMenu(this.menu)
}
updateMenuStates (visibleStates, enabledStates, checkedStates) {
updateStates(this.items, visibleStates, enabledStates, checkedStates)
this.updateContextMenu()
}
updateMenuItemVisibleState (id, flag) {
@@ -144,7 +291,67 @@ export default class TrayManager extends EventEmitter {
this.updateMenuStates(null, enabledStates, null)
}
handleLocaleChange (locale) {
this.setupMenu()
}
handleSpeedometerEnableChange (enabled) {
this.toggleSpeedometer(enabled)
this.renderTray()
}
handleSystemThemeChange (systemTheme = APP_THEME.LIGHT) {
if (!is.macOS()) {
return
}
this.systemTheme = systemTheme
this.inverseSystemTheme = getInverseTheme(systemTheme)
this.loadImages()
this.renderTray()
}
handleDownloadStatusChange (status) {
this.status = status
this.renderTray()
}
async handleSpeedChange ({ uploadSpeed, downloadSpeed }) {
if (!this.speedometer) {
return
}
this.uploadSpeed = uploadSpeed
this.downloadSpeed = downloadSpeed
await this.renderTray()
}
async updateTrayByImage (ab) {
const buffer = convertArrayBufferToBuffer(ab)
const image = nativeImage.createFromBuffer(buffer, {
scaleFactor: 2
})
image.setTemplateImage(this.bigSur)
tray.setImage(image)
}
destroy () {
if (tray) {
tray.removeListener('click', this.handleTrayClick)
// tray.removeListener('double-click', this.handleTrayDbClick)
tray.removeListener('right-click', this.handleTrayRightClick)
tray.removeListener('mouse-down', this.handleTrayMouseDown)
tray.removeListener('mouse-up', this.handleTrayMouseUp)
tray.removeListener('drop-files', this.handleTrayDropFiles)
tray.removeListener('drop-text', this.handleTrayDropText)
}
tray.destroy()
}
}
+34 -10
View File
@@ -12,8 +12,7 @@ const defaultBrowserOptions = {
width: 1024,
height: 768,
webPreferences: {
nodeIntegration: true,
webviewTag: true
nodeIntegration: true
}
}
@@ -82,7 +81,14 @@ export default class WindowManager extends EventEmitter {
window = new BrowserWindow({
...defaultBrowserOptions,
...pageOptions.attrs
...pageOptions.attrs,
webPreferences: {
enableRemoteModule: true,
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
},
hasShadow: !is.macOS()
})
const bounds = this.getPageBounds(page)
@@ -105,6 +111,14 @@ export default class WindowManager extends EventEmitter {
}
})
window.on('enter-full-screen', () => {
this.emit('enter-full-screen', window)
})
window.on('leave-full-screen', () => {
this.emit('leave-full-screen', window)
})
this.handleWindowState(page, window)
this.handleWindowClose(pageOptions, page, window)
@@ -169,7 +183,15 @@ export default class WindowManager extends EventEmitter {
window.on('close', (event) => {
if (pageOptions.bindCloseToHide && !this.willQuit) {
event.preventDefault()
window.hide()
// @see https://github.com/electron/electron/issues/20263
if (window.isFullScreen()) {
window.once('leave-full-screen', () => window.hide())
window.setFullScreen(false)
} else {
window.hide()
}
}
const bounds = window.getBounds()
this.emit('window-closed', { page, bounds })
@@ -178,15 +200,16 @@ export default class WindowManager extends EventEmitter {
showWindow (page) {
const window = this.getWindow(page)
if (!window) {
if (!window || window.isVisible()) {
return
}
window.show()
}
autoHideWindow (page) {
hideWindow (page) {
const window = this.getWindow(page)
if (!window) {
if (!window || !window.isVisible()) {
return
}
window.hide()
@@ -203,10 +226,11 @@ export default class WindowManager extends EventEmitter {
if (!window) {
return
}
if (window.isVisible()) {
window.hide()
} else {
if (!window.isVisible() || window.isFullScreen()) {
window.show()
} else {
window.hide()
}
}
+27 -7
View File
@@ -1,17 +1,18 @@
import { app } from 'electron'
import { app, nativeTheme } from 'electron'
import is from 'electron-is'
import { resolve } from 'path'
import { existsSync, lstatSync } from 'fs'
import {
APP_THEME,
ENGINE_MAX_CONNECTION_PER_SERVER,
IP_VERSION
} from '@shared/constants'
import logger from '../core/Logger'
import engineBinMap from '../configs/engine'
export function getLogPath () {
return logger.transports.file.file
return app.getPath('logs')
}
export function getDhtPath (protocol) {
@@ -23,6 +24,10 @@ export function getSessionPath () {
return resolve(app.getPath('userData'), './download.session')
}
export function getEnginePidPath () {
return resolve(app.getPath('userData'), './engine.pid')
}
export function getUserDataPath () {
return app.getPath('userData')
}
@@ -100,13 +105,13 @@ export function parseArgvAsUrl (argv) {
export function checkIsSupportedSchema (url = '') {
const str = url.toLowerCase()
if (
str.startsWith('mo:') ||
str.startsWith('motrix:') ||
str.startsWith('ftp:') ||
str.startsWith('http:') ||
str.startsWith('https:') ||
str.startsWith('ftp:') ||
str.startsWith('magnet:') ||
str.startsWith('thunder:')
str.startsWith('thunder:') ||
str.startsWith('mo:') ||
str.startsWith('motrix:')
) {
return true
} else {
@@ -133,3 +138,18 @@ export function parseArgvAsFile (argv) {
export const getMaxConnectionPerServer = () => {
return ENGINE_MAX_CONNECTION_PER_SERVER
}
export const getSystemTheme = () => {
let result = APP_THEME.LIGHT
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
export const convertArrayBufferToBuffer = (arrayBuffer) => {
const buffer = Buffer.alloc(arrayBuffer.byteLength)
const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i]
}
return buffer
}
+8 -4
View File
@@ -1,3 +1,5 @@
import { parse } from 'querystring'
export function concat (template, submenu, submenuToAdd) {
submenuToAdd.forEach(sub => {
let relativeItem = null
@@ -115,16 +117,18 @@ function handleCommandBefore (item) {
if (!item['command-before']) {
return
}
const [command, ...args] = item['command-before'].split(',')
global.application.sendCommandToAll(command, ...args)
const [command, params] = item['command-before'].split('?')
const args = parse(params)
global.application.sendCommandToAll(command, args)
}
function handleCommandAfter (item) {
if (!item['command-after']) {
return
}
const [command, ...args] = item['command-after'].split(',')
global.application.sendCommandToAll(command, ...args)
const [command, params] = item['command-after'].split('?')
const args = parse(params)
global.application.sendCommandToAll(command, args)
}
function acceleratorForCommand (command, keystrokesByCommand) {
+51 -33
View File
@@ -1,7 +1,7 @@
import { ipcRenderer, remote } from 'electron'
import { ipcRenderer } from 'electron'
import is from 'electron-is'
import { isEmpty } from 'lodash'
import Aria2 from 'aria2'
import { isEmpty, clone } from 'lodash'
import { Aria2 } from '@shared/aria2'
import {
separateConfig,
compactUndefined,
@@ -12,19 +12,18 @@ import {
} from '@shared/utils'
import { ENGINE_RPC_HOST } from '@shared/constants'
const application = remote.getGlobal('application')
export default class Api {
constructor (options = {}) {
this.options = options
this.client = null
this.init()
}
init () {
this.loadConfig()
this.initClient()
async init () {
this.config = await this.loadConfig()
this.client = this.initClient()
this.client.open()
}
loadConfigFromLocalStorage () {
@@ -33,21 +32,18 @@ export default class Api {
return result
}
loadConfigFromNativeStore () {
const systemConfig = application.configManager.getSystemConfig()
const userConfig = application.configManager.getUserConfig()
const result = { ...systemConfig, ...userConfig }
async loadConfigFromNativeStore () {
const result = await ipcRenderer.invoke('get-app-config')
return result
}
loadConfig () {
async loadConfig () {
let result = is.renderer()
? this.loadConfigFromNativeStore()
? await this.loadConfigFromNativeStore()
: this.loadConfigFromLocalStorage()
result = changeKeysToCamelCase(result)
this.config = result
return result
}
initClient () {
@@ -56,12 +52,11 @@ export default class Api {
rpcSecret: secret
} = this.config
const host = ENGINE_RPC_HOST
this.client = new Aria2({
return new Aria2({
host,
port,
secret
})
this.client.open()
}
closeClient () {
@@ -76,7 +71,7 @@ export default class Api {
fetchPreference () {
return new Promise((resolve) => {
this.loadConfig()
this.config = this.loadConfig()
resolve(this.config)
})
}
@@ -160,11 +155,10 @@ export default class Api {
}
changeOption (params = {}) {
let { gid, options = {} } = params
options = formatOptionsForEngine(options)
const { gid, options = {} } = params
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([gid, kebabOptions])
const engineOptions = formatOptionsForEngine(options)
const args = compactUndefined([gid, engineOptions])
return this.client.call('changeOption', ...args)
}
@@ -180,11 +174,11 @@ export default class Api {
options
} = params
const tasks = uris.map((uri, index) => {
const kebabOptions = changeKeysToKebabCase(options)
const engineOptions = formatOptionsForEngine(options)
if (outs && outs[index]) {
kebabOptions.out = outs[index]
engineOptions.out = outs[index]
}
const args = compactUndefined([[uri], kebabOptions])
const args = compactUndefined([[uri], engineOptions])
return ['aria2.addUri', ...args]
})
return this.client.multicall(tasks)
@@ -195,8 +189,8 @@ export default class Api {
torrent,
options
} = params
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([torrent, [], kebabOptions])
const engineOptions = formatOptionsForEngine(options)
const args = compactUndefined([torrent, [], engineOptions])
return this.client.call('addTorrent', ...args)
}
@@ -205,8 +199,8 @@ export default class Api {
metalink,
options
} = params
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([metalink, kebabOptions])
const engineOptions = formatOptionsForEngine(options)
const args = compactUndefined([metalink, engineOptions])
return this.client.call('addMetalink', ...args)
}
@@ -261,6 +255,30 @@ export default class Api {
return this.client.call('tellStatus', ...args)
}
fetchTaskItemWithPeers (params = {}) {
const { gid, keys } = params
const statusArgs = compactUndefined([gid, keys])
const peersArgs = compactUndefined([gid])
return new Promise((resolve, reject) => {
this.client.multicall([
['aria2.tellStatus', ...statusArgs],
['aria2.getPeers', ...peersArgs]
]).then((data) => {
console.log('[Motrix] fetchTaskItemWithPeers:', data)
const result = data[0] && data[0][0]
const peers = data[1] && data[1][0]
result.peers = peers || []
console.log('[Motrix] fetchTaskItemWithPeers.result:', result)
console.log('[Motrix] fetchTaskItemWithPeers.peers:', peers)
resolve(result)
}).catch((err) => {
console.log('[Motrix] fetch downloading task list fail:', err)
reject(err)
})
})
}
fetchTaskItemPeers (params = {}) {
const { gid, keys } = params
const args = compactUndefined([gid, keys])
@@ -328,8 +346,8 @@ export default class Api {
options = formatOptionsForEngine(options)
const data = gids.map((gid, index) => {
const kebabOptions = changeKeysToKebabCase(options)
const args = compactUndefined([gid, kebabOptions])
const _options = clone(options)
const args = compactUndefined([gid, _options])
return [method, ...args]
})
return this.client.multicall(data)
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g stroke-linecap="square" stroke-linejoin="miter" stroke-width="2" fill="#000000" stroke="#000000">
<line fill="none" stroke-miterlimit="10" x1="8.2" y1="4.5" x2="11.1" y2="7.3"></line>
<line fill="none" stroke-miterlimit="10" x1="16.7" y1="12.9" x2="19.5" y2="15.8"></line>
<path fill="none" stroke="#000000" stroke-miterlimit="10"
d="M12.5,17.2 c-1.6,1.6-4.1,1.6-5.7,0c-1.6-1.6-1.6-4.1,0-5.7l7.1-7.1l-2.8-2.8L4,8.7C0.9,11.8,0.9,16.9,4,20s8.2,3.1,11.3,0l7.1-7.1l-2.8-2.8 L12.5,17.2z">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 617 B

+2 -1
View File
@@ -15,6 +15,7 @@
import { mapState } from 'vuex'
import AppInfo from '@/components/About/AppInfo'
import Copyright from '@/components/About/Copyright'
import { app } from '@electron/remote'
export default {
name: 'mo-about-panel',
@@ -29,7 +30,7 @@
}
},
data () {
const version = this.$electron.remote.app.getVersion()
const version = app.getVersion()
return {
version
}
+1 -1
View File
@@ -2,7 +2,7 @@
<el-row class="copyright">
<el-col :span="6" class="copyright-left">
<a target="_blank" rel="noopener noreferrer" href="https://motrix.app/">
&copy;2019 Motrix
&copy;2021 Motrix
</a>
</el-col>
<el-col :span="18" class="copyright-right">
+1 -1
View File
@@ -41,7 +41,7 @@
...mapState('app', {
currentPage: state => state.currentPage
}),
asideDraggable: function () {
asideDraggable () {
return is.macOS()
}
},
+12 -15
View File
@@ -1,21 +1,18 @@
<template>
<div ref="webviewViewport" class="webview-viewport">
<webview
<iframe
class="mo-webview"
ref="webview"
ref="iframe"
:src="src"
></webview>
></iframe>
</div>
</template>
<script>
import is from 'electron-is'
import { webContents } from '@electron/remote'
import { Loading } from 'element-ui'
import {
openExternal
} from '@/components/Native/utils'
export default {
name: 'mo-browser',
components: {
@@ -35,11 +32,11 @@
isRenderer: () => is.renderer()
},
mounted () {
const { webview } = this.$refs
const { iframe } = this.$refs
webview.addEventListener('did-start-loading', this.loadStart.bind(this))
webview.addEventListener('did-stop-loading', this.loadStop.bind(this))
webview.addEventListener('dom-ready', this.ready.bind(this))
iframe.addEventListener('did-start-loading', this.loadStart.bind(this))
iframe.addEventListener('did-stop-loading', this.loadStop.bind(this))
iframe.addEventListener('dom-ready', this.ready.bind(this))
},
methods: {
loadStart () {
@@ -54,12 +51,12 @@
})
},
ready () {
const { webview } = this.$refs
const { iframe } = this.$refs
const webContents = this.$electron.remote.webContents.fromId(webview.getWebContentsId())
webContents.on('new-window', (event, url) => {
const wc = webContents.fromId(iframe.getWebContentsId())
wc.setWindowOpenHandler((event, url) => {
event.preventDefault()
openExternal(url)
this.$electron.ipcRenderer.send('command', 'application:open-external', url)
})
}
}
-115
View File
@@ -1,115 +0,0 @@
import { Message } from 'element-ui'
import { base64StringToBlob } from 'blob-util'
import router from '@/router'
import store from '@/store'
import { buildFileList } from '@shared/utils'
import { ADD_TASK_TYPE } from '@shared/constants'
import { getLocaleManager } from '@/components/Locale'
import CommandManager from './CommandManager'
const commands = new CommandManager()
const i18n = getLocaleManager().getI18n()
function updateSystemTheme (theme) {
store.dispatch('app/updateSystemTheme', theme)
}
function updateTheme (theme) {
store.dispatch('preference/changeThemeConfig', theme)
}
function showAboutPanel () {
store.dispatch('app/showAboutPanel')
}
function showAddTask (taskType = ADD_TASK_TYPE.URI, task = '') {
if (taskType === ADD_TASK_TYPE.URI && task) {
store.dispatch('app/updateAddTaskUrl', task)
}
store.dispatch('app/showAddTaskDialog', taskType)
}
function showAddBtTask () {
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
}
function showAddBtTaskWithFile (fileName, base64Data = '') {
const blob = base64StringToBlob(base64Data, 'application/x-bittorrent')
const file = new File([blob], fileName, { type: 'application/x-bittorrent' })
const fileList = buildFileList(file)
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
setTimeout(() => {
store.dispatch('app/addTaskAddTorrents', { fileList })
}, 200)
}
function navigateTaskList (status = 'active') {
router.push({ path: `/task/${status}` }).catch(err => {
console.log(err)
})
}
function navigatePreferences () {
router.push({ path: '/preference' }).catch(err => {
console.log(err)
})
}
function showUnderDevelopmentMessage () {
Message.info(i18n.t('app.under-development-message'))
}
function pauseTask () {
store.dispatch('task/batchPauseSelectedTasks')
}
function resumeTask () {
store.dispatch('task/batchResumeSelectedTasks')
}
function deleteTask () {
showUnderDevelopmentMessage()
}
function moveTaskUp () {
showUnderDevelopmentMessage()
}
function moveTaskDown () {
showUnderDevelopmentMessage()
}
function pauseAllTask () {
store.dispatch('task/pauseAllTask')
}
function resumeAllTask () {
store.dispatch('task/resumeAllTask')
}
function selectAllTask () {
store.dispatch('task/selectAllTask')
}
commands.register('application:system-theme', updateSystemTheme)
commands.register('application:theme', updateTheme)
commands.register('application:about', showAboutPanel)
commands.register('application:new-task', showAddTask)
commands.register('application:new-bt-task', showAddBtTask)
commands.register('application:new-bt-task-with-file', showAddBtTaskWithFile)
commands.register('application:task-list', navigateTaskList)
commands.register('application:preferences', navigatePreferences)
commands.register('application:pause-task', pauseTask)
commands.register('application:resume-task', resumeTask)
commands.register('application:delete-task', deleteTask)
commands.register('application:move-task-up', moveTaskUp)
commands.register('application:move-task-down', moveTaskDown)
commands.register('application:pause-all-task', pauseAllTask)
commands.register('application:resume-all-task', resumeAllTask)
commands.register('application:select-all-task', selectAllTask)
export {
commands
}
@@ -9,11 +9,11 @@ export default class CommandManager extends EventEmitter {
register (id, fn) {
if (this.commands[id]) {
console.log('Attempting to register an already-registered command: ' + id)
console.log('[Motrix] Attempting to register an already-registered command: ' + id)
return null
}
if (!id || !fn) {
console.error('Attempting to register a command with a missing id, or command function.')
console.error('[Motrix] Attempting to register a command with a missing id, or command function.')
return null
}
this.commands[id] = fn
@@ -21,8 +21,16 @@ export default class CommandManager extends EventEmitter {
this.emit('commandRegistered', id)
}
unregister (id) {
if (this.commands[id]) {
delete this.commands[id]
this.emit('commandUnregistered', id)
}
}
execute (id, ...args) {
var fn = this.commands[id]
const fn = this.commands[id]
if (fn) {
try {
this.emit('beforeExecuteCommand', id)
@@ -0,0 +1,3 @@
import CommandManager from '.'
export const commands = new CommandManager()
+20
View File
@@ -0,0 +1,20 @@
import Icon from '@/components/Icons/Icon'
Icon.register({
'magnet': {
'width': 24,
'height': 24,
'raw': `
<line fill="none" stroke-miterlimit="10" x1="8.2" y1="4.5" x2="11.1" y2="7.3"></line>
<line fill="none" stroke-miterlimit="10" x1="16.7" y1="12.9" x2="19.5" y2="15.8"></line>
<path fill="none" stroke-miterlimit="10"
d="M12.5,17.2 c-1.6,1.6-4.1,1.6-5.7,0c-1.6-1.6-1.6-4.1,0-5.7l7.1-7.1l-2.8-2.8L4,8.7C0.9,11.8,0.9,16.9,4,20s8.2,3.1,11.3,0l7.1-7.1l-2.8-2.8 L12.5,17.2z">
</path>`,
'g': {
'stroke': 'currentColor',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2'
}
}
})
+1
View File
@@ -37,6 +37,7 @@
display: block;
width: 100%;
height: 100%;
outline: none;
text-align: center;
font-size: 0;
color: $--app-logo-color;
+14 -5
View File
@@ -5,7 +5,13 @@
<mo-speedometer />
<mo-add-task :visible="addTaskVisible" :type="addTaskType" />
<mo-about-panel :visible="aboutPanelVisible" />
<mo-task-item-info :visible="taskItemInfoVisible" :task="currentTaskItem" />
<mo-task-detail
:visible="taskDetailVisible"
:gid="currentTaskGid"
:task="currentTaskItem"
:files="currentTaskFiles"
:peers="currentTaskPeers"
/>
<mo-dragger />
</el-container>
</template>
@@ -16,7 +22,7 @@
import Aside from '@/components/Aside/Index'
import Speedometer from '@/components/Speedometer/Speedometer'
import AddTask from '@/components/Task/AddTask'
import TaskItemInfo from '@/components/Task/TaskItemInfo'
import TaskDetail from '@/components/TaskDetail/Index'
import Dragger from '@/components/Dragger/Index'
export default {
@@ -26,7 +32,7 @@
[Aside.name]: Aside,
[Speedometer.name]: Speedometer,
[AddTask.name]: AddTask,
[TaskItemInfo.name]: TaskItemInfo,
[TaskDetail.name]: TaskDetail,
[Dragger.name]: Dragger
},
computed: {
@@ -36,8 +42,11 @@
addTaskType: state => state.addTaskType
}),
...mapState('task', {
taskItemInfoVisible: state => state.taskItemInfoVisible,
currentTaskItem: state => state.currentTaskItem
taskDetailVisible: state => state.taskDetailVisible,
currentTaskGid: state => state.currentTaskGid,
currentTaskItem: state => state.currentTaskItem,
currentTaskFiles: state => state.currentTaskFiles,
currentTaskPeers: state => state.currentTaskPeers
})
},
methods: {
@@ -0,0 +1,107 @@
<template>
<div style="display: none;">
<img
id="tray-icon-light-normal"
src="static/mo-tray-light-normal@2x.png"
>
<img
id="tray-icon-light-active"
src="static/mo-tray-light-active@2x.png"
>
<img
id="tray-icon-dark-normal"
src="static/mo-tray-dark-normal@2x.png"
>
<img
id="tray-icon-dark-active"
src="static/mo-tray-dark-active@2x.png"
>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getInverseTheme } from '@shared/utils'
import { APP_THEME } from '@shared/constants'
const cache = {}
export default {
name: 'mo-dynamic-tray',
computed: {
...mapState('app', {
bigSur: state => state.bigSur,
iconStatus: state => state.stat.numActive > 0 ? 'active' : 'normal',
theme: state => state.systemTheme,
focused: state => state.trayFocused,
uploadSpeed: state => state.stat.uploadSpeed,
downloadSpeed: state => state.stat.downloadSpeed,
speed: state => state.stat.uploadSpeed + state.stat.downloadSpeed
}),
scale () {
return 2
},
currentTheme () {
const { theme, focused } = this
if (theme === APP_THEME.DARK) {
return theme
}
return focused ? getInverseTheme(theme) : theme
},
iconKey () {
const { bigSur, iconStatus, currentTheme } = this
return bigSur ? 'tray-icon-light-normal' : `tray-icon-${currentTheme}-${iconStatus}`
}
},
watch: {
async speed (val) {
await this.drawTray()
},
async iconKey (val) {
await this.drawTray()
}
},
mounted () {
setTimeout(async () => {
await this.drawTray()
}, 200)
},
methods: {
async getIcon (key) {
if (cache[key]) {
return cache[key]
}
const iconImage = document.getElementById(key)
const result = await createImageBitmap(iconImage)
cache[key] = result
return result
},
async drawTray () {
const {
currentTheme: theme,
uploadSpeed,
downloadSpeed,
scale,
iconKey
} = this
const icon = await this.getIcon(iconKey)
global.app.trayWorker.postMessage({
type: 'tray:draw',
payload: {
theme,
icon,
uploadSpeed,
downloadSpeed,
scale
}
})
}
}
}
</script>
+51 -34
View File
@@ -7,44 +7,44 @@
import { mapState } from 'vuex'
import api from '@/api'
import {
showItemInFolder,
addToRecentTask
} from '@/components/Native/utils'
import {
bytesToSize,
getTaskName,
getTaskFullPath
} from '@shared/utils'
getTaskFullPath,
showItemInFolder
} from '@/utils/native'
import { checkTaskIsBT, getTaskName } from '@shared/utils'
export default {
name: 'mo-engine-client',
data: function () {
return {
downloading: false
}
},
computed: {
isRenderer: () => is.renderer(),
...mapState('app', {
uploadSpeed: state => state.stat.uploadSpeed,
downloadSpeed: state => state.stat.downloadSpeed,
speed: state => state.stat.uploadSpeed + state.stat.downloadSpeed,
interval: state => state.interval,
numActive: state => state.stat.numActive
downloading: state => state.stat.numActive > 0
}),
...mapState('task', {
taskItemInfoVisible: state => state.taskItemInfoVisible,
messages: state => state.messages,
seedingList: state => state.seedingList,
taskDetailVisible: state => state.taskDetailVisible,
enabledFetchPeers: state => state.enabledFetchPeers,
currentTaskGid: state => state.currentTaskGid,
currentTaskItem: state => state.currentTaskItem
}),
...mapState('preference', {
taskNotification: state => state.config.taskNotification
})
}),
currentTaskIsBT () {
return checkTaskIsBT(this.currentTaskItem)
}
},
watch: {
downloadSpeed (val, oldVal) {
const speed = val > 0 ? `${bytesToSize(val)}/s` : ''
this.$electron.ipcRenderer.send('event', 'download-speed-change', speed)
},
numActive (val, oldVal) {
this.downloading = val > 0
speed (val) {
const { uploadSpeed, downloadSpeed } = this
this.$electron.ipcRenderer.send('event', 'speed-change', {
uploadSpeed,
downloadSpeed
})
},
downloading (val, oldVal) {
if (val !== oldVal && this.isRenderer) {
@@ -62,8 +62,13 @@
onDownloadStart (event) {
this.$store.dispatch('task/fetchList')
this.$store.dispatch('app/resetInterval')
console.log('aria2 onDownloadStart', event)
this.$store.dispatch('task/saveSession')
const [{ gid }] = event
const { seedingList } = this
if (seedingList.includes(gid)) {
return
}
this.fetchTaskItem({ gid })
.then((task) => {
const taskName = getTaskName(task)
@@ -72,8 +77,12 @@
})
},
onDownloadPause (event) {
console.log('aria2 onDownloadPause')
const [{ gid }] = event
const { seedingList } = this
if (seedingList.includes(gid)) {
return
}
this.fetchTaskItem({ gid })
.then((task) => {
const taskName = getTaskName(task)
@@ -82,7 +91,6 @@
})
},
onDownloadStop (event) {
console.log('aria2 onDownloadStop')
const [{ gid }] = event
this.fetchTaskItem({ gid })
.then((task) => {
@@ -99,7 +107,7 @@
const { errorCode, errorMessage } = task
console.error(`[Motrix] download error gid: ${gid}, #${errorCode}, ${errorMessage}`)
const message = this.$t('task.download-error-message', { taskName })
const link = `<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/Error#${errorCode}" rel="noopener noreferrer">#${errorCode}</a>`
const link = `<a target="_blank" href="https://github.com/agalwood/Motrix/wiki/Error#${errorCode}" rel="noopener noreferrer">${errorCode}</a>`
this.$msg({
type: 'error',
showClose: true,
@@ -110,30 +118,35 @@
})
},
onDownloadComplete (event) {
console.log('aria2 onDownloadComplete')
this.$store.dispatch('task/fetchList')
const [{ gid }] = event
this.$store.dispatch('task/removeFromSeedingList', gid)
this.fetchTaskItem({ gid })
.then((task) => {
this.handleDownloadComplete(task, false)
})
},
onBtDownloadComplete (event) {
console.log('aria2 onBtDownloadComplete')
this.$store.dispatch('task/fetchList')
const [{ gid }] = event
const { seedingList } = this
if (seedingList.includes(gid)) {
return
}
this.$store.dispatch('task/addToSeedingList', gid)
this.fetchTaskItem({ gid })
.then((task) => {
this.handleDownloadComplete(task, true)
})
},
handleDownloadComplete (task, isBT) {
this.$store.dispatch('task/saveSession')
const path = getTaskFullPath(task)
this.showTaskCompleteNotify(task, isBT, path)
addToRecentTask(task)
this.$electron.ipcRenderer.send('event', 'task-download-complete', task, path)
},
showTaskCompleteNotify (task, isBT, path) {
@@ -206,8 +219,12 @@
this.$store.dispatch('app/fetchGlobalStat')
this.$store.dispatch('task/fetchList')
if (this.taskItemInfoVisible && this.currentTaskItem) {
this.$store.dispatch('task/fetchItem', this.currentTaskItem.gid)
if (this.taskDetailVisible && this.currentTaskGid) {
if (this.currentTaskIsBT && this.enabledFetchPeers) {
this.$store.dispatch('task/fetchItemWithPeers', this.currentTaskGid)
} else {
this.$store.dispatch('task/fetchItem', this.currentTaskGid)
}
}
},
stopPolling () {
+5 -25
View File
@@ -3,44 +3,24 @@
</template>
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import {
commands
} from '@/components/Command/index'
import { commands } from '@/components/CommandManager/instance'
export default {
name: 'mo-ipc',
computed: {
...mapState('preference', {
enableEggFeatures: state => state.config.enableEggFeatures
})
},
watch: {
},
methods: {
bindIpcEvents: function () {
bindIpcEvents () {
this.$electron.ipcRenderer.on('command', (event, command, ...args) => {
commands.execute(command, ...args)
})
},
unbindIpcEvents: function () {
unbindIpcEvents () {
this.$electron.ipcRenderer.removeAllListeners('command')
}
},
created: function () {
created () {
this.bindIpcEvents()
// id of the menu item
const visibleStates = {}
if (is.mas()) {
visibleStates['app.check-for-updates'] = false
if (!this.enableEggFeatures) {
visibleStates['task.new-bt-task'] = false
}
}
this.$electron.ipcRenderer.send('command', 'application:change-menu-states', visibleStates, null, null)
},
destroyed: function () {
destroyed () {
this.unbindIpcEvents()
}
}
@@ -8,6 +8,7 @@
</template>
<script>
import { dialog } from '@electron/remote'
import '@/components/Icons/folder'
export default {
@@ -15,9 +16,9 @@
props: {
},
methods: {
onFolderClick: function () {
onFolderClick () {
const self = this
this.$electron.remote.dialog.showOpenDialog({
dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory']
}).then(({ canceled, filePaths }) => {
if (canceled || filePaths.length === 0) {
@@ -8,7 +8,7 @@
import '@/components/Icons/folder'
import {
showItemInFolder
} from '@/components/Native/utils'
} from '@/utils/native'
export default {
name: 'mo-show-in-folder',
@@ -20,7 +20,7 @@
computed: {
},
methods: {
onFolderClick: function () {
onFolderClick () {
if (!this.path) {
return
}
+9 -6
View File
@@ -16,6 +16,7 @@
</template>
<script>
import { getCurrentWindow } from '@electron/remote'
import '@/components/Icons/win-minimize'
import '@/components/Icons/win-maximize'
import '@/components/Icons/win-close'
@@ -28,22 +29,22 @@
}
},
computed: {
win: function () {
return this.$electron.remote.getCurrentWindow()
win () {
return getCurrentWindow()
}
},
methods: {
handleMinimize: function () {
handleMinimize () {
this.win.minimize()
},
handleMaximize: function () {
handleMaximize () {
if (this.win.isMaximized()) {
this.win.unmaximize()
} else {
this.win.maximize()
}
},
handleClose: function () {
handleClose () {
this.win.close()
}
}
@@ -73,9 +74,11 @@
padding: 0;
margin: 0;
z-index: 5100;
font-size: 0;
> li {
display: inline-block;
padding: 5px 15px;
padding: 5px 18px;
font-size: 16px;
margin: 0;
color: $--titlebar-actions-color;
&:hover {
-152
View File
@@ -1,152 +0,0 @@
import is from 'electron-is'
import { access, constants } from 'fs'
import { Message } from 'element-ui'
import {
isMagnetTask,
getTaskFullPath,
bytesToSize
} from '@shared/utils'
import { APP_THEME, TASK_STATUS } from '@shared/constants'
const remote = is.renderer() ? require('electron').remote : {}
export function getUserDownloadsPath () {
return remote.app.getPath('downloads')
}
export function prettifyDir (dir) {
const downloads = getUserDownloadsPath()
const result = dir === downloads ? 'Downloads' : dir
return result
}
export function showItemInFolder (fullPath, { errorMsg }) {
if (!fullPath) {
return
}
access(fullPath, constants.F_OK, (err) => {
console.log(`${fullPath} ${err ? 'does not exist' : 'exists'}`)
if (err) {
Message.error(errorMsg)
return
}
remote.shell.showItemInFolder(fullPath)
})
}
export function openItem (fullPath, { errorMsg }) {
if (!fullPath) {
return
}
const result = remote.shell.openItem(fullPath)
if (!result && errorMsg) {
Message.error(errorMsg)
}
return result
}
export function moveTaskFilesToTrash (task) {
/**
* For magnet link tasks, there is bittorrent, but there is no bittorrent.info.
* The path is not a complete path before it becomes a BT task.
* In order to avoid accidentally deleting the directory
* where the task is located, it directly returns true when deleting.
*/
if (isMagnetTask(task)) {
return true
}
const { dir, status } = task
const path = getTaskFullPath(task)
if (!path || dir === path) {
throw new Error('task.file-path-error')
}
let deleteResult1 = true
access(path, constants.F_OK, (err) => {
console.log(`${path} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult1 = remote.shell.moveItemToTrash(path)
}
})
// There is no configuration file for the completed task.
if (status === TASK_STATUS.COMPLETE) {
return deleteResult1
}
let deleteResult2 = true
const extraFilePath = `${path}.aria2`
access(extraFilePath, constants.F_OK, (err) => {
console.log(`${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
deleteResult2 = remote.shell.moveItemToTrash(extraFilePath)
}
})
return deleteResult1 && deleteResult2
}
export function openDownloadDock (path) {
if (!is.macOS()) {
return
}
remote.app.dock.downloadFinished(path)
}
export function updateDockBadge (text) {
if (!is.macOS()) {
return
}
remote.app.dock.setBadge(text)
}
export function showDownloadSpeedInDock (downloadSpeed) {
if (!is.macOS()) {
return
}
const text = downloadSpeed > 0 ? `${bytesToSize(downloadSpeed)}/s` : ''
updateDockBadge(text)
}
export function addToRecentTask (task) {
if (is.linux()) {
return
}
const path = getTaskFullPath(task)
remote.app.addRecentDocument(path)
}
export function addToRecentTaskByPath (path) {
if (is.linux()) {
return
}
remote.app.addRecentDocument(path)
}
export function clearRecentTasks () {
if (is.linux()) {
return
}
remote.app.clearRecentDocuments()
}
export function getSystemTheme () {
let result = APP_THEME.LIGHT
if (!is.macOS()) {
return result
}
result = remote.nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
export const openExternal = (url, options) => {
if (!url) {
return
}
remote.shell.openExternal(url, options)
}
+49 -11
View File
@@ -94,6 +94,8 @@
<el-select
class="select-track-source"
v-model="form.trackerSource"
allow-create
filterable
multiple
>
<el-option-group
@@ -105,7 +107,18 @@
v-for="item in group.options"
:key="item.value"
:label="item.label"
:value="item.value">
:value="item.value"
>
<span style="float: left">{{ item.label }}</span>
<span style="float: right; margin-right: 24px">
<el-tag
type="success"
size="mini"
v-if="item.cdn"
>
CDN
</el-tag>
</span>
</el-option>
</el-option-group>
</el-select>
@@ -169,10 +182,10 @@
:label-width="formLabelWidth"
>
<el-row style="margin-bottom: 8px;">
<el-col class="form-item-sub" :span="10">
<el-col class="form-item-sub" :span="12">
<el-switch
v-model="form.enableUpnp"
active-text="UPnP"
active-text="UPnP/NAT-PMP"
>
</el-switch>
</el-col>
@@ -292,6 +305,9 @@
</el-input>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-button plain type="warning" @click="() => onSessionResetClick()">
{{ $t('preferences.session-reset') }}
</el-button>
<el-button plain type="danger" @click="() => onFactoryResetClick()">
{{ $t('preferences.factory-reset') }}
</el-button>
@@ -317,6 +333,7 @@
<script>
import is from 'electron-is'
import { dialog } from '@electron/remote'
import { mapState } from 'vuex'
import { cloneDeep } from 'lodash'
import randomize from 'randomatic'
@@ -339,7 +356,7 @@
import '@/components/Icons/sync'
import '@/components/Icons/refresh'
const initialForm = (config) => {
const initForm = (config) => {
const {
allProxy,
allProxyBackup,
@@ -393,7 +410,7 @@
},
data () {
const { locale } = this.$store.state.preference.config
const form = initialForm(this.$store.state.preference.config)
const form = initForm(this.$store.state.preference.config)
const formOriginal = cloneDeep(form)
return {
@@ -407,11 +424,11 @@
}
},
computed: {
isRenderer () { return is.renderer() },
isRenderer: () => is.renderer(),
title () {
return this.$t('preferences.advanced')
},
subnavs: function () {
subnavs () {
return [
{
key: 'basic',
@@ -463,8 +480,9 @@
const tracker = convertTrackerDataToLine(data)
this.form.lastSyncTrackerTime = Date.now()
this.form.btTracker = tracker
this.trackerSyncing = false
})
.finally(() => {
.catch((_) => {
this.trackerSyncing = false
})
},
@@ -505,8 +523,25 @@
this.hideRpcSecret = true
}, 2000)
},
onSessionResetClick () {
dialog.showMessageBox({
type: 'warning',
title: this.$t('preferences.session-reset'),
message: this.$t('preferences.session-reset-confirm'),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1
}).then(({ response }) => {
if (response === 0) {
this.$store.dispatch('task/purgeTaskRecord')
this.$store.dispatch('task/pauseAllTask')
.then(() => {
this.$electron.ipcRenderer.send('command', 'application:reset-session')
})
}
})
},
onFactoryResetClick () {
this.$electron.remote.dialog.showMessageBox({
dialog.showMessageBox({
type: 'warning',
title: this.$t('preferences.factory-reset'),
message: this.$t('preferences.factory-reset-confirm'),
@@ -521,14 +556,14 @@
syncFormConfig () {
this.$store.dispatch('preference/fetchPreference')
.then((config) => {
this.form = initialForm(config)
this.form = initForm(config)
this.formOriginal = cloneDeep(this.form)
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('[Motrix] preference form valid:', valid)
console.error('[Motrix] preference form valid:', valid)
return false
}
@@ -589,6 +624,9 @@
.select-track-source {
width: 100%;
}
.el-select__tags {
overflow-x: auto;
}
}
}
.ua-group {
+100 -17
View File
@@ -37,8 +37,14 @@
{{ $t('preferences.auto-hide-window') }}
</el-checkbox>
</el-col>
<el-col v-if="isMac" class="form-item-sub" :span="16">
<el-checkbox v-model="form.traySpeedometer">
{{ $t('preferences.tray-speedometer') }}
</el-checkbox>
</el-col>
</el-form-item>
<el-form-item
v-if="isMac"
:label="`${$t('preferences.run-mode')}: `"
:label-width="formLabelWidth"
>
@@ -137,6 +143,49 @@
</el-select>
</el-col>
</el-form-item>
<el-form-item
:label="`${$t('preferences.bt-settings')}: `"
:label-width="formLabelWidth"
>
<el-col class="form-item-sub" :span="24">
<el-checkbox v-model="form.btSaveMetadata">
{{ $t('preferences.bt-save-metadata') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-switch
v-model="form.keepSeeding"
:active-text="$t('preferences.keep-seeding')"
@change="onKeepSeedingChange"
>
</el-switch>
</el-col>
<el-col class="form-item-sub" :span="24" v-if="!form.keepSeeding">
{{ $t('preferences.seed-ratio') }}
<el-input-number
v-model="form.seedRatio"
controls-position="right"
:min="1"
:max="100"
:step="0.1"
:label="$t('preferences.seed-ratio')">
</el-input-number>
</el-col>
<el-col class="form-item-sub" :span="24" v-if="!form.keepSeeding">
{{ $t('preferences.seed-time') }}
({{ $t('preferences.seed-time-unit') }})
<el-input-number
v-model="form.seedTime"
controls-position="right"
:min="60"
:max="525600"
:step="1"
:label="$t('preferences.seed-time')">
</el-input-number>
</el-col>
<div class="el-form-item__info" style="margin-top: 8px;">
</div>
</el-form-item>
<el-form-item
:label="`${$t('preferences.task-manage')}: `"
:label-width="formLabelWidth"
@@ -176,6 +225,11 @@
{{ $t('preferences.task-completed-notify') }}
</el-checkbox>
</el-col>
<el-col class="form-item-sub" :span="24">
<el-checkbox v-model="form.noConfirmBeforeDeleteTask">
{{ $t('preferences.no-confirm-before-delete-task') }}
</el-checkbox>
</el-col>
</el-form-item>
</el-form>
<div class="form-actions">
@@ -204,7 +258,6 @@
import ThemeSwitcher from '@/components/Preference/ThemeSwitcher'
import { availableLanguages, getLanguage } from '@shared/locales'
import { getLocaleManager } from '@/components/Locale'
import { prettifyDir } from '@/components/Native/utils'
import {
calcFormLabelWidth,
checkIsNeedRestart,
@@ -212,12 +265,14 @@
} from '@shared/utils'
import { APP_RUN_MODE } from '@shared/constants'
const initialForm = (config) => {
const initForm = (config) => {
const {
autoHideWindow,
btSaveMetadata,
dir,
engineMaxConnectionPerServer,
hideAppMenu,
keepSeeding,
keepWindowState,
locale,
maxConcurrentDownloads,
@@ -225,18 +280,24 @@
maxOverallDownloadLimit,
maxOverallUploadLimit,
newTaskShowDownloading,
noConfirmBeforeDeleteTask,
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
seedRatio,
seedTime,
taskNotification,
theme
theme,
traySpeedometer
} = config
const result = {
autoHideWindow,
btSaveMetadata,
continue: config.continue,
dir,
engineMaxConnectionPerServer,
hideAppMenu,
keepSeeding,
keepWindowState,
locale,
maxConcurrentDownloads,
@@ -244,11 +305,15 @@
maxOverallDownloadLimit,
maxOverallUploadLimit,
newTaskShowDownloading,
noConfirmBeforeDeleteTask,
openAtLogin,
resumeAllWhenAppLaunched,
runMode,
seedRatio,
seedTime,
taskNotification,
theme
theme,
traySpeedometer
}
return result
}
@@ -262,7 +327,7 @@
},
data () {
const { locale } = this.$store.state.preference.config
const form = initialForm(this.$store.state.preference.config)
const form = initForm(this.$store.state.preference.config)
const formOriginal = cloneDeep(form)
return {
@@ -274,8 +339,9 @@
}
},
computed: {
isRenderer () { return is.renderer() },
isMas () { return is.mas() },
isRenderer: () => is.renderer(),
isMac: () => is.macOS(),
isMas: () => is.mas(),
isLinux () { return is.linux() },
title () {
return this.$t('preferences.basic')
@@ -314,6 +380,14 @@
label: '1 MB/s',
value: '1M'
},
{
label: '2 MB/s',
value: '2M'
},
{
label: '3 MB/s',
value: '3M'
},
{
label: '5 MB/s',
value: '5M'
@@ -321,10 +395,14 @@
{
label: '10 MB/s',
value: '10M'
},
{
label: '20 MB/s',
value: '20M'
}
]
},
subnavs: function () {
subnavs () {
return [
{
key: 'basic',
@@ -346,9 +424,6 @@
showHideAppMenuOption () {
return is.windows() || is.linux()
},
downloadDir () {
return prettifyDir(this.form.dir)
},
...mapState('preference', {
config: state => state.config
})
@@ -365,20 +440,24 @@
this.$electron.ipcRenderer.send('command',
'application:change-theme', theme)
},
onKeepSeedingChange (enable) {
this.form.seedRatio = enable ? 0 : 1
this.form.seedTime = enable ? 525600 : 60
},
onDirectorySelected (dir) {
this.form.dir = dir
},
syncFormConfig () {
this.$store.dispatch('preference/fetchPreference')
.then((config) => {
this.form = initialForm(config)
this.form = initForm(config)
this.formOriginal = cloneDeep(this.form)
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('[Motrix] preference form valid:', valid)
console.error('[Motrix] preference form valid:', valid)
return false
}
@@ -403,11 +482,15 @@
this.$electron.ipcRenderer.send('command',
'application:open-at-login', openAtLogin)
this.$electron.ipcRenderer.send('command',
'application:toggle-dock', runMode === APP_RUN_MODE.STANDARD)
if ('runMode' in changed) {
this.$electron.ipcRenderer.send('command',
'application:toggle-dock', runMode === APP_RUN_MODE.STANDARD)
}
this.$electron.ipcRenderer.send('command',
'application:auto-hide-window', autoHideWindow)
if ('autoHideWindow' in changed) {
this.$electron.ipcRenderer.send('command',
'application:auto-hide-window', autoHideWindow)
}
if (checkIsNeedRestart(data)) {
this.$electron.ipcRenderer.send('command',
+4 -10
View File
@@ -10,11 +10,8 @@
<script>
export default {
name: 'mo-content-preference',
computed: {
},
components: {
},
methods: {
created () {
this.$store.dispatch('preference/fetchPreference')
}
}
</script>
@@ -59,16 +56,13 @@
}
}
.form-actions {
position: fixed;
position: sticky;
bottom: 0;
left: auto;
z-index: 10;
width: -webkit-fill-available;
box-sizing: border-box;
padding: 24px 36px;
margin-left: -36px;
// aside.width + subnav.width + padding-left + scrollbar.width
margin-right: 322px;
padding: 24px 36px 24px 0;
}
.action-link {
cursor: pointer;
+1 -1
View File
@@ -41,7 +41,7 @@
title () {
return this.$t('preferences.lab')
},
subnavs: function () {
subnavs () {
return [
{
key: 'basic',
@@ -27,7 +27,7 @@
default: APP_THEME.AUTO
}
},
data: function () {
data () {
return {
currentValue: this.value
}
@@ -54,7 +54,7 @@
}
},
watch: {
currentValue: function (val) {
currentValue (val) {
this.$emit('change', val)
}
},
@@ -47,12 +47,12 @@
}
},
computed: {
title: function () {
title () {
return this.$t('subnav.preferences')
}
},
methods: {
nav: function (category = 'basic') {
nav (category = 'basic') {
this.$router.push({
path: `/preference/${category}`
}).catch(err => {
@@ -47,12 +47,12 @@
}
},
computed: {
title: function () {
title () {
return this.$t('subnav.task-list')
}
},
methods: {
nav: function (status = 'active') {
nav (status = 'active') {
this.$router.push({
path: `/task/${status}`
}).catch(err => {
+45 -132
View File
@@ -49,7 +49,7 @@
:label-width="formLabelWidth"
>
<el-input-number
v-model="form.maxConnectionPerServer"
v-model="form.split"
controls-position="right"
:min="1"
:max="config.engineMaxConnectionPerServer"
@@ -172,39 +172,15 @@
import { isEmpty } from 'lodash'
import SelectDirectory from '@/components/Native/SelectDirectory'
import SelectTorrent from '@/components/Task/SelectTorrent'
import { prettifyDir } from '@/components/Native/utils'
import { ADD_TASK_TYPE, NONE_SELECTED_FILES, SELECTED_ALL_FILES } from '@shared/constants'
import { detectResource, splitTaskLinks } from '@shared/utils'
import { buildOuts } from '@shared/utils/rename'
import {
initTaskForm,
buildUriPayload,
buildTorrentPayload
} from '@/utils/task'
import { ADD_TASK_TYPE } from '@shared/constants'
import { detectResource } from '@shared/utils'
import '@/components/Icons/inbox'
const initialForm = state => {
const { addTaskUrl, addTaskOptions } = state.app
const {
allProxy,
dir,
engineMaxConnectionPerServer,
maxConnectionPerServer,
newTaskShowDownloading
} = state.preference.config
const result = {
allProxy,
cookie: '',
dir,
engineMaxConnectionPerServer,
maxConnectionPerServer,
newTaskShowDownloading,
out: '',
referer: '',
selectFile: NONE_SELECTED_FILES,
torrent: '',
uris: addTaskUrl,
userAgent: '',
...addTaskOptions
}
return result
}
export default {
name: 'mo-add-task',
components: {
@@ -230,23 +206,20 @@
}
},
computed: {
isRenderer () { return is.renderer() },
isMas () { return is.mas() },
taskType: function () {
return this.type
},
downloadDir: function () {
return prettifyDir(this.form.dir)
},
isRenderer: () => is.renderer(),
isMas: () => is.mas(),
...mapState('app', {
taskList: state => state.taskList
}),
...mapState('preference', {
config: state => state.config
})
}),
taskType () {
return this.type
}
},
watch: {
taskType: function (current, previous) {
taskType (current, previous) {
if (this.visible && previous === ADD_TASK_TYPE.URI) {
return
}
@@ -256,18 +229,16 @@
this.$refs.uri && this.$refs.uri.focus()
}, 50)
}
},
visible (current) {
if (current === true) {
document.addEventListener('keydown', this.handleHotkey)
} else {
document.removeEventListener('keydown', this.handleHotkey)
}
}
},
methods: {
handleOpen () {
this.form = initialForm(this.$store.state)
if (this.taskType === ADD_TASK_TYPE.URI) {
this.autofillResourceLink()
setTimeout(() => {
this.$refs.uri && this.$refs.uri.focus()
}, 50)
}
},
autofillResourceLink () {
const content = this.$electron.clipboard.readText()
const hasResource = detectResource(content)
@@ -278,9 +249,21 @@
this.form.uris = content
}
},
handleOpen () {
this.form = initTaskForm(this.$store.state)
if (this.taskType === ADD_TASK_TYPE.URI) {
this.autofillResourceLink()
setTimeout(() => {
this.$refs.uri && this.$refs.uri.focus()
}, 50)
}
},
handleOpened () {
this.detectThunderResource(this.form.uris)
},
handleCancel (formName) {
this.$store.dispatch('app/hideAddTaskDialog')
},
handleClose (done) {
this.$store.dispatch('app/hideAddTaskDialog')
this.$store.dispatch('app/updateAddTaskOptions', {})
@@ -288,6 +271,13 @@
handleClosed () {
this.reset()
},
handleHotkey (event) {
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault()
this.submitForm('taskForm')
}
},
handleTabClick (tab, event) {
this.$store.dispatch('app/changeAddTaskType', tab.name)
},
@@ -315,94 +305,17 @@
},
reset () {
this.showAdvanced = false
this.form = initialForm(this.$store.state)
},
handleCancel (formName) {
this.$store.dispatch('app/hideAddTaskDialog')
},
buildHeader (form) {
const { userAgent, referer, cookie } = form
const result = []
if (!isEmpty(userAgent)) {
result.push(`User-Agent: ${userAgent}`)
}
if (!isEmpty(referer)) {
result.push(`Referer: ${referer}`)
}
if (!isEmpty(cookie)) {
result.push(`Cookie: ${cookie}`)
}
return result
},
buildOption (type, form) {
const { allProxy, dir, out, selectFile } = form
const result = {}
if (!isEmpty(allProxy)) {
result.allProxy = allProxy
}
if (!isEmpty(dir)) {
result.dir = dir
}
if (!isEmpty(out)) {
result.out = out
}
if (type === ADD_TASK_TYPE.TORRENT) {
if (
selectFile !== SELECTED_ALL_FILES &&
selectFile !== NONE_SELECTED_FILES
) {
result.selectFile = selectFile
}
}
const header = this.buildHeader(form)
if (!isEmpty(header)) {
result.header = header
}
return result
},
buildUriPayload (form) {
let { uris, out } = form
if (isEmpty(uris)) {
throw new Error(this.$t('task.new-task-uris-required'))
}
uris = splitTaskLinks(uris)
const outs = buildOuts(uris, out)
const options = this.buildOption(ADD_TASK_TYPE.URI, form)
const result = {
uris,
outs,
options
}
return result
},
buildTorrentPayload (form) {
const { torrent } = form
if (isEmpty(torrent)) {
throw new Error(this.$t('task.new-task-torrent-required'))
}
const options = this.buildOption(ADD_TASK_TYPE.TORRENT, form)
const result = {
torrent,
options
}
return result
this.form = initTaskForm(this.$store.state)
},
addTask (type, form) {
let payload = null
if (type === ADD_TASK_TYPE.URI) {
payload = this.buildUriPayload(form)
payload = buildUriPayload(form)
this.$store.dispatch('task/addUri', payload).catch(err => {
this.$msg.error(err.message)
})
} else if (type === ADD_TASK_TYPE.TORRENT) {
payload = this.buildTorrentPayload(form)
payload = buildTorrentPayload(form)
this.$store.dispatch('task/addTorrent', payload).catch(err => {
this.$msg.error(err.message)
})
@@ -430,7 +343,7 @@
})
}
} catch (err) {
this.$msg.error(err.message)
this.$msg.error(this.$t(err.message))
}
})
}
+320 -3
View File
@@ -33,10 +33,25 @@
</template>
<script>
import { dialog } from '@electron/remote'
import { mapState } from 'vuex'
import * as clipboard from 'clipboard-polyfill'
import { commands } from '@/components/CommandManager/instance'
import { ADD_TASK_TYPE } from '@shared/constants'
import TaskSubnav from '@/components/Subnav/TaskSubnav'
import TaskActions from '@/components/Task/TaskActions'
import TaskList from '@/components/Task/TaskList'
import SubnavSwitcher from '@/components/Subnav/SubnavSwitcher'
import {
getTaskUri,
parseHeader
} from '@shared/utils'
import {
delayDeleteTaskFiles,
showItemInFolder,
moveTaskFilesToTrash
} from '@/utils/native'
export default {
name: 'mo-content-task',
@@ -53,7 +68,15 @@
}
},
computed: {
subnavs: function () {
...mapState('task', {
taskList: state => state.taskList,
selectedGidList: state => state.selectedGidList,
selectedGidListCount: state => state.selectedGidList.length
}),
...mapState('preference', {
noConfirmBeforeDelete: state => state.config.noConfirmBeforeDeleteTask
}),
subnavs () {
return [
{
key: 'active',
@@ -72,7 +95,7 @@
}
]
},
title: function () {
title () {
const subnav = this.subnavs.find((item) => item.key === this.status)
return subnav.title
}
@@ -86,10 +109,304 @@
},
changeCurrentList () {
this.$store.dispatch('task/changeCurrentList', this.status)
},
directAddTask (uri, options = {}) {
const uris = [uri]
const payload = {
uris,
options: {
...options
}
}
this.$store.dispatch('task/addUri', payload)
.catch((err) => {
this.$msg.error(err.message)
})
},
showAddTaskDialog (uri, options = {}) {
const {
header,
...rest
} = options
console.log('[Motrix] show add task dialog options: ', options)
const headers = parseHeader(header)
const newOptions = {
...rest,
...headers
}
this.$store.dispatch('app/updateAddTaskUrl', uri)
this.$store.dispatch('app/updateAddTaskOptions', newOptions)
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
},
deleteTaskFiles (task) {
try {
const result = moveTaskFilesToTrash(task)
if (!result) {
throw new Error('task.remove-task-file-fail')
}
} catch (err) {
this.$msg.error(this.$t(err.message))
}
},
removeTask (task, taskName, isRemoveWithFiles = false) {
this.$store.dispatch('task/forcePauseTask', task)
.finally(() => {
if (isRemoveWithFiles) {
this.deleteTaskFiles(task)
}
return this.removeTaskItem(task, taskName)
})
},
removeTaskRecord (task, taskName, isRemoveWithFiles = false) {
this.$store.dispatch('task/forcePauseTask', task)
.finally(() => {
if (isRemoveWithFiles) {
this.deleteTaskFiles(task)
}
return this.removeTaskRecordItem(task, taskName)
})
},
async removeTaskItem (task, taskName) {
try {
await this.$store.dispatch('task/removeTask', task)
this.$msg.success(this.$t('task.delete-task-success', {
taskName
}))
} catch ({ code }) {
if (code === 1) {
this.$msg.error(this.$t('task.delete-task-fail', {
taskName
}))
}
}
},
async removeTaskRecordItem (task, taskName) {
try {
await this.$store.dispatch('task/removeTaskRecord', task)
this.$msg.success(this.$t('task.remove-record-success', {
taskName
}))
} catch ({ code }) {
if (code === 1) {
this.$msg.error(this.$t('task.remove-record-fail', {
taskName
}))
}
}
},
removeTasks (taskList, isRemoveWithFiles = false) {
const gids = taskList.map((task) => task.gid)
this.$store.dispatch('task/batchForcePauseTask', gids)
.finally(() => {
if (isRemoveWithFiles) {
this.batchDeleteTaskFiles(taskList)
}
this.removeTaskItems(gids)
})
},
batchDeleteTaskFiles (taskList) {
const promises = taskList.map((task, index) => delayDeleteTaskFiles(task, index * 200))
Promise.allSettled(promises).then(results => {
console.log('[Motrix] batch delete task files: ', results)
})
},
removeTaskItems (gids) {
this.$store.dispatch('task/batchRemoveTask', gids)
.then(() => {
this.$msg.success(this.$t('task.batch-delete-task-success'))
})
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.batch-delete-task-fail'))
}
})
},
handlePauseTask (payload) {
const { task, taskName } = payload
this.$msg.info(this.$t('task.download-pause-message', { taskName }))
this.$store.dispatch('task/pauseTask', task)
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.pause-task-fail', { taskName }))
}
})
},
handleResumeTask (payload) {
const { task, taskName } = payload
this.$store.dispatch('task/resumeTask', task)
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.resume-task-fail', {
taskName
}))
}
})
},
handleStopTaskSeeding (payload) {
const { task } = payload
this.$store.dispatch('task/stopSeeding', task)
this.$msg.info({
message: this.$t('task.bt-stopping-seeding-tip'),
duration: 8000
})
},
handleRestartTask (payload) {
const { task, taskName, showDialog } = payload
const { gid } = task
const uri = getTaskUri(task)
this.$store.dispatch('task/getTaskOption', gid)
.then((data) => {
console.log('[Motrix] get task option:', data)
const { dir, header, split } = data
const options = {
dir,
header,
split,
out: taskName
}
if (showDialog) {
this.showAddTaskDialog(uri, options)
} else {
this.directAddTask(uri, options)
this.$store.dispatch('task/removeTaskRecord', task)
}
})
},
handleRevealInFolder (payload) {
const { path } = payload
showItemInFolder(path, {
errorMsg: this.$t('task.file-not-exist')
})
},
handleDeleteTask (payload) {
const { task, taskName, deleteWithFiles } = payload
const { noConfirmBeforeDelete } = this
if (noConfirmBeforeDelete) {
this.removeTask(task, taskName, deleteWithFiles)
return
}
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-task'),
message: this.$t('task.delete-task-confirm', { taskName }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.delete-task-label'),
checkboxChecked: deleteWithFiles
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
this.removeTask(task, taskName, checkboxChecked)
}
})
},
handleDeleteTaskRecord (payload) {
const { task, taskName, deleteWithFiles } = payload
const { noConfirmBeforeDelete } = this
if (noConfirmBeforeDelete) {
this.removeTaskRecord(task, taskName, deleteWithFiles)
return
}
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.remove-record'),
message: this.$t('task.remove-record-confirm', { taskName }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.remove-record-label'),
checkboxChecked: !!deleteWithFiles
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
this.removeTaskRecord(task, taskName, checkboxChecked)
}
})
},
handleBatchDeleteTask (payload) {
const { deleteWithFiles } = payload
const {
noConfirmBeforeDelete,
selectedGidList,
selectedGidListCount,
taskList
} = this
if (selectedGidListCount === 0) {
return
}
const selectedTaskList = taskList.filter((task) => {
return selectedGidList.includes(task.gid)
})
if (noConfirmBeforeDelete) {
this.removeTasks(selectedTaskList, deleteWithFiles)
return
}
const count = `${selectedGidListCount}`
dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-selected-task'),
message: this.$t('task.batch-delete-task-confirm', { count }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.delete-task-label'),
checkboxChecked: deleteWithFiles
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
this.removeTasks(selectedTaskList, checkboxChecked)
}
})
},
handleCopyTaskLink (payload) {
const { task } = payload
const uri = getTaskUri(task)
clipboard.writeText(uri)
.then(() => {
this.$msg.success(this.$t('task.copy-link-success'))
})
},
handleShowTaskInfo (payload) {
const { task } = payload
this.$store.dispatch('task/showTaskDetail', task)
}
},
created: function () {
created () {
this.changeCurrentList()
},
mounted () {
commands.on('pause-task', this.handlePauseTask)
commands.on('resume-task', this.handleResumeTask)
commands.on('stop-task-seeding', this.handleStopTaskSeeding)
commands.on('restart-task', this.handleRestartTask)
commands.on('reveal-in-folder', this.handleRevealInFolder)
commands.on('delete-task', this.handleDeleteTask)
commands.on('delete-task-record', this.handleDeleteTaskRecord)
commands.on('batch-delete-task', this.handleBatchDeleteTask)
commands.on('copy-task-link', this.handleCopyTaskLink)
commands.on('show-task-info', this.handleShowTaskInfo)
},
destroyed () {
commands.off('pause-task', this.handlePauseTask)
commands.off('resume-task', this.handleResumeTask)
commands.off('stop-task-seeding', this.handleStopTaskSeeding)
commands.off('restart-task', this.handleRestartTask)
commands.off('reveal-in-folder', this.handleRevealInFolder)
commands.off('delete-task', this.handleDeleteTask)
commands.off('delete-task-record', this.handleDeleteTaskRecord)
commands.off('batch-delete-task', this.handleBatchDeleteTask)
commands.off('copy-task-link', this.handleCopyTaskLink)
commands.off('show-task-info', this.handleShowTaskInfo)
}
}
</script>
+66 -178
View File
@@ -28,74 +28,28 @@
</el-tooltip>
</el-col>
<el-col class="torrent-actions" :span="4">
<span
@click="handleTrashClick"
>
<span @click="handleTrashClick">
<mo-icon name="trash" width="14" height="14" />
</span>
</el-col>
</el-row>
<div class="torrent-file-list">
<el-table
stripe
ref="torrentTable"
height="200"
:data="files"
tooltip-effect="dark"
style="width: 100%"
@row-dblclick="handleRowDbClick"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="42">
</el-table-column>
<el-table-column
:label="$t('task.file-name')"
show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>
<el-table-column
:label="$t('task.file-extension')"
width="80">
<template slot-scope="scope">{{ scope.row.extension | removeExtensionDot }}</template>
</el-table-column>
<el-table-column
:label="$t('task.file-size')"
width="90">
<template slot-scope="scope">{{ scope.row.length | bytesToSize }}</template>
</el-table-column>
</el-table>
</div>
<el-row :gutter="12">
<el-col class="file-filters" :span="8">
<el-button-group>
<el-button @click="toggleVideoSelection()">
<mo-icon name="video" width="12" height="12" />
</el-button>
<el-button @click="toggleAudioSelection()">
<mo-icon name="audio" width="12" height="12" />
</el-button>
<el-button @click="toggleImageSelection()">
<mo-icon name="image" width="12" height="12" />
</el-button>
</el-button-group>
</el-col>
<el-col :span="16" style="text-align: right">
{{ $t('task.selected-files-sum', { selectedFilesCount, selectedFilesTotalSize }) }}
</el-col>
</el-row>
<mo-task-files
ref="torrentFileList"
mode="ADD"
:files="files"
:height="200"
@selection-change="handleSelectionChange"
/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { isEmpty } from 'lodash'
import parseTorrent from 'parse-torrent'
import TaskFiles from '@/components/TaskDetail/TaskFiles'
import '@/components/Icons/inbox'
import '@/components/Icons/video'
import '@/components/Icons/audio'
import '@/components/Icons/image'
import {
EMPTY_STRING,
NONE_SELECTED_FILES,
SELECTED_ALL_FILES
} from '@shared/constants'
@@ -103,9 +57,6 @@
buildFileList,
listTorrentFiles,
bytesToSize,
filterVideoFiles,
filterAudioFiles,
filterImageFiles,
getAsBase64,
removeExtensionDot
} from '@shared/utils'
@@ -113,6 +64,7 @@
export default {
name: 'mo-select-torrent',
components: {
[TaskFiles.name]: TaskFiles
},
filters: {
bytesToSize,
@@ -122,42 +74,21 @@
},
data () {
return {
name: '',
currentTorrent: '',
name: EMPTY_STRING,
currentTorrent: EMPTY_STRING,
files: [],
selectedFiles: []
}
},
computed: {
...mapState('preference', {
config: state => state.config
}),
...mapState('app', {
torrents: state => state.addTaskTorrents
}),
isTorrentsEmpty: function () {
...mapState('preference', {
config: state => state.config
}),
isTorrentsEmpty () {
return this.torrents.length === 0
},
selectedFilesCount: function () {
return this.selectedFiles.length
},
selectedFilesTotalSize: function () {
const result = this.selectedFiles.reduce((acc, cur) => {
return acc + cur.length
}, 0)
return bytesToSize(result)
},
selectedFileIndex: function () {
const { files, selectedFiles } = this
if (files.length === 0 || selectedFiles.length === 0) {
return NONE_SELECTED_FILES
}
if (files.length === selectedFiles.length) {
return SELECTED_ALL_FILES
}
const indexArr = this.selectedFiles.map((item) => item.idx)
const result = indexArr.join(',')
return result
}
},
watch: {
@@ -174,9 +105,9 @@
parseTorrent.remote(file.raw, (err, parsedTorrent) => {
if (err) throw err
console.log(parsedTorrent)
console.log('[Motrix] parsed torrent: ', parsedTorrent)
this.files = listTorrentFiles(parsedTorrent.files)
this.$refs.torrentTable.toggleAllSelection()
this.$refs.torrentFileList.toggleAllSelection()
getAsBase64(file.raw, (torrent) => {
this.name = file.name
@@ -184,21 +115,17 @@
this.$emit('change', torrent, SELECTED_ALL_FILES)
})
})
},
selectedFileIndex () {
const { currentTorrent, selectedFileIndex } = this
this.$emit('change', currentTorrent, selectedFileIndex)
}
},
methods: {
reset () {
this.name = ''
this.currentTorrent = ''
this.name = EMPTY_STRING
this.currentTorrent = EMPTY_STRING
this.files = []
if (this.$refs.torrentTable) {
this.$refs.torrentTable.clearSelection()
if (this.$refs.torrentFileList) {
this.$refs.torrentFileList.clearSelection()
}
this.$emit('change', '', NONE_SELECTED_FILES)
this.$emit('change', EMPTY_STRING, NONE_SELECTED_FILES)
},
handleChange (file, fileList) {
this.$store.dispatch('app/addTaskAddTorrents', { fileList })
@@ -210,96 +137,57 @@
handleTrashClick () {
this.$store.dispatch('app/addTaskAddTorrents', { fileList: [] })
},
toggleSelection (rows) {
if (isEmpty(rows)) {
this.$refs.torrentTable.clearSelection()
} else {
this.$refs.torrentTable.clearSelection()
rows.forEach(row => {
this.$refs.torrentTable.toggleRowSelection(row)
})
}
},
toggleVideoSelection () {
const filtered = filterVideoFiles(this.files)
this.toggleSelection(filtered)
},
toggleAudioSelection () {
const filtered = filterAudioFiles(this.files)
this.toggleSelection(filtered)
},
toggleImageSelection () {
const filtered = filterImageFiles(this.files)
this.toggleSelection(filtered)
},
handleRowDbClick (row, column, event) {
this.$refs.torrentTable.toggleRowSelection(row)
},
handleSelectionChange (val) {
this.selectedFiles = val
const { currentTorrent } = this
this.$emit('change', currentTorrent, val)
}
}
}
</script>
<style lang="scss">
.upload-torrent {
.upload-torrent {
width: 100%;
.el-upload, .el-upload-dragger {
width: 100%;
.el-upload, .el-upload-dragger {
width: 100%;
}
.el-upload-dragger {
border-radius: 4px;
padding: 24px;
height: auto;
}
.upload-inbox-icon {
}
.el-upload-dragger {
border-radius: 4px;
padding: 24px;
height: auto;
}
.upload-inbox-icon {
display: inline-block;
margin-bottom: 12px;
}
.torrent-name {
margin-top: 4px;
font-size: $--font-size-small;
color: $--color-text-secondary;
line-height: 16px;
}
}
.selective-torrent {
.torrent-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.torrent-info {
margin-bottom: 15px;
font-size: 12px;
line-height: 16px;
}
.torrent-actions {
text-align: right;
line-height: 16px;
&> span {
cursor: pointer;
display: inline-block;
margin-bottom: 12px;
}
.torrent-name {
margin-top: 4px;
font-size: $--font-size-small;
color: $--color-text-secondary;
line-height: 16px;
}
}
.selective-torrent {
.torrent-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.torrent-info {
margin-bottom: 15px;
font-size: 12px;
line-height: 16px;
}
.torrent-actions {
text-align: right;
line-height: 16px;
&> span {
cursor: pointer;
display: inline-block;
vertical-align: middle;
height: 14px;
padding: 1px;
}
}
}
.file-filters {
button {
font-size: 0;
}
}
.torrent-file-list {
border: 1px solid #ebeef5;
border-bottom: none;
overflow-x: hidden;
overflow-y: scroll;
margin-bottom: 8px;
.el-table th {
padding: 2px 0;
vertical-align: middle;
height: 14px;
padding: 1px;
}
}
}
</style>
+33 -84
View File
@@ -3,8 +3,8 @@
<el-tooltip
class="item hidden-md-and-up"
effect="dark"
:content="$t('task.new-task')"
placement="bottom"
:content="$t('task.new-task')"
>
<i class="task-action" @click.stop="onAddClick">
<mo-icon name="menu-add" width="14" height="14" />
@@ -15,6 +15,7 @@
effect="dark"
placement="bottom"
:content="$t('task.delete-selected-tasks')"
v-if="currentList !== 'stopped'"
>
<i
class="task-action"
@@ -23,17 +24,32 @@
<mo-icon name="delete" width="14" height="14" />
</i>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="$t('task.refresh-list')" placement="bottom">
<el-tooltip
class="item"
effect="dark"
placement="bottom"
:content="$t('task.refresh-list')"
>
<i class="task-action" @click="onRefreshClick">
<mo-icon name="refresh" width="14" height="14" :spin="refreshing" />
</i>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="$t('task.resume-all-task')" placement="bottom">
<el-tooltip
class="item"
effect="dark"
placement="bottom"
:content="$t('task.resume-all-task')"
>
<i class="task-action" @click="onResumeAllClick">
<mo-icon name="task-start-line" width="14" height="14" />
</i>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="$t('task.pause-all-task')" placement="bottom">
<el-tooltip
class="item"
effect="dark"
placement="bottom"
:content="$t('task.pause-all-task')"
>
<i class="task-action" @click="onPauseAllClick">
<mo-icon name="task-pause-line" width="14" height="14" />
</i>
@@ -41,8 +57,8 @@
<el-tooltip
class="item"
effect="dark"
:content="$t('task.purge-record')"
placement="bottom"
:content="$t('task.purge-record')"
v-if="currentList === 'stopped'"
>
<i class="task-action" @click="onPurgeRecordClick">
@@ -54,11 +70,10 @@
<script>
import { mapState } from 'vuex'
import { commands } from '@/components/CommandManager/instance'
import { ADD_TASK_TYPE } from '@shared/constants'
import { bytesToSize, timeFormat } from '@shared/utils'
import {
moveTaskFilesToTrash
} from '@/components/Native/utils'
import '@/components/Icons/menu-add'
import '@/components/Icons/refresh'
import '@/components/Icons/task-start-line'
@@ -72,7 +87,7 @@
components: {
},
props: ['task'],
data: function () {
data () {
return {
refreshing: false
}
@@ -80,8 +95,6 @@
computed: {
...mapState('task', {
currentList: state => state.currentList,
taskList: state => state.taskList,
selectedGidList: state => state.selectedGidList,
selectedGidListCount: state => state.selectedGidList.length
})
},
@@ -90,7 +103,7 @@
timeFormat
},
methods: {
refreshSpin: function () {
refreshSpin () {
this.t && clearTimeout(this.t)
this.refreshing = true
@@ -98,79 +111,15 @@
this.refreshing = false
}, 500)
},
delayDeleteTaskFiles (task, delay) {
return new Promise((resolve) => {
setTimeout(() => {
try {
const result = moveTaskFilesToTrash(task)
resolve(result)
} catch (err) {
console.log('[Motrix] batch delay delete task files fail', err)
resolve(false)
}
}, delay)
})
onBatchDeleteClick (event) {
const deleteWithFiles = !!event.shiftKey
commands.emit('batch-delete-task', { deleteWithFiles })
},
batchDeleteTaskFiles (taskList) {
const promises = taskList.map((task, index) => this.delayDeleteTaskFiles(task, index * 200))
Promise.all(promises).then(values => {
console.log(values)
})
},
removeTasks (taskList, isRemoveWithFiles) {
const gids = taskList.map((task) => task.gid)
this.$store.dispatch('task/batchForcePauseTask', gids)
.finally(() => {
if (isRemoveWithFiles) {
this.batchDeleteTaskFiles(taskList)
}
this.removeTaskItems(gids)
})
},
removeTaskItems (gids) {
this.$store.dispatch('task/batchRemoveTask', gids)
.then(() => {
this.$msg.success(this.$t('task.batch-delete-task-success'))
})
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.batch-delete-task-fail'))
}
})
},
onBatchDeleteClick: function (event) {
const self = this
const { taskList, selectedGidList, selectedGidListCount } = this
if (selectedGidListCount === 0) {
event.preventDefault()
return
}
const selectedTaskList = taskList.filter((task) => {
return selectedGidList.includes(task.gid)
})
const count = `${selectedGidListCount}`
const isChecked = !!event.shiftKey
this.$electron.remote.dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-selected-task'),
message: this.$t('task.batch-delete-task-confirm', { count }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.delete-task-label'),
checkboxChecked: isChecked
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
self.removeTasks(selectedTaskList, checkboxChecked)
}
})
},
onRefreshClick: function () {
onRefreshClick () {
this.refreshSpin()
this.$store.dispatch('task/fetchList')
},
onResumeAllClick: function () {
onResumeAllClick () {
this.$store.dispatch('task/resumeAllTask')
.then(() => {
this.$msg.success(this.$t('task.resume-all-task-success'))
@@ -181,7 +130,7 @@
}
})
},
onPauseAllClick: function () {
onPauseAllClick () {
this.$store.dispatch('task/pauseAllTask')
.then(() => {
this.$msg.success(this.$t('task.pause-all-task-success'))
@@ -192,7 +141,7 @@
}
})
},
onPurgeRecordClick: function () {
onPurgeRecordClick () {
this.$store.dispatch('task/purgeTaskRecord')
.then(() => {
this.$msg.success(this.$t('task.purge-record-success'))
@@ -203,7 +152,7 @@
}
})
},
onAddClick: function () {
onAddClick () {
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
}
}
+9 -11
View File
@@ -16,12 +16,9 @@
</template>
<script>
import {
getTaskFullPath,
getTaskName
} from '@shared/utils'
import { getTaskName } from '@shared/utils'
import { TASK_STATUS } from '@shared/constants'
import { openItem } from '@/components/Native/utils'
import { openItem, getTaskFullPath } from '@/utils/native'
import TaskItemActions from './TaskItemActions'
import TaskProgress from './TaskProgress'
import TaskProgressInfo from './TaskProgressInfo'
@@ -61,13 +58,14 @@
this.toggleTask()
}
},
openTask () {
async openTask () {
const { taskName } = this
this.$msg.info(this.$t('task.opening-task-message', { taskName }))
const fullPath = getTaskFullPath(this.task)
openItem(fullPath, {
errorMsg: this.$t('task.file-not-exist')
})
const result = await openItem(fullPath)
if (result) {
this.$msg.error(this.$t('task.file-not-exist'))
}
},
toggleTask () {
this.$store.dispatch('task/toggleTask', this.task)
@@ -100,8 +98,8 @@
}
.task-name {
color: #505753;
margin-bottom: 32px;
margin-right: 240px;
margin-bottom: 1.5rem;
margin-right: 220px;
word-break: break-all;
min-height: 26px;
&> span {
+91 -218
View File
@@ -36,20 +36,16 @@
</template>
<script>
import { mapState } from 'vuex'
import is from 'electron-is'
import * as clipboard from 'clipboard-polyfill'
import { ADD_TASK_TYPE, TASK_STATUS } from '@shared/constants'
import {
showItemInFolder,
moveTaskFilesToTrash
} from '@/components/Native/utils'
import { commands } from '@/components/CommandManager/instance'
import { TASK_STATUS } from '@shared/constants'
import {
checkTaskIsSeeder,
getTaskFullPath,
getTaskName,
getTaskUri,
parseHeader
getTaskName
} from '@shared/utils'
import { getTaskFullPath } from '@/utils/native'
import '@/components/Icons/task-start-line'
import '@/components/Icons/task-pause-line'
import '@/components/Icons/task-stop-line'
@@ -76,7 +72,10 @@
props: {
mode: {
type: String,
default: 'LIST'
default: 'LIST',
validator: function (value) {
return ['LIST', 'DETAIL'].indexOf(value) !== -1
}
},
task: {
type: Object,
@@ -84,6 +83,9 @@
}
},
computed: {
...mapState('preference', {
noConfirmBeforeDelete: state => state.config.noConfirmBeforeDeleteTask
}),
taskName () {
return getTaskName(this.task)
},
@@ -102,10 +104,17 @@
}
},
taskCommonActions () {
let result = is.renderer() ? ['FOLDER'] : []
result = (this.mode === 'LIST')
? [...result, 'LINK', 'INFO']
: [...result, 'LINK']
const { mode } = this
const result = is.renderer() ? ['FOLDER'] : []
switch (mode) {
case 'LIST':
result.push('LINK', 'INFO')
break
case 'DETAIL':
result.push('LINK')
break
}
return result
},
@@ -117,206 +126,67 @@
}
},
methods: {
deleteTaskFiles (task) {
try {
const result = moveTaskFilesToTrash(task)
if (!result) {
throw new Error('task.remove-task-file-fail')
}
} catch (err) {
this.$msg.error(this.$t(err.message))
}
},
removeTask (task, isRemoveWithFiles) {
this.$store.dispatch('task/forcePauseTask', task)
.finally(() => {
if (isRemoveWithFiles) {
this.deleteTaskFiles(task)
}
return this.removeTaskItem(task)
})
},
removeTaskItem (task) {
return this.$store.dispatch('task/removeTask', this.task)
.then(() => {
this.$msg.success(this.$t('task.delete-task-success', {
taskName: this.taskName
}))
})
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.delete-task-fail', {
taskName: this.taskName
}))
}
})
},
removeTaskRecord (task, isRemoveWithFiles) {
this.$store.dispatch('task/forcePauseTask', task)
.finally(() => {
if (isRemoveWithFiles) {
this.deleteTaskFiles(task)
}
return this.removeTaskRecordItem(task)
})
},
removeTaskRecordItem (task) {
return this.$store.dispatch('task/removeTaskRecord', this.task)
.then(() => {
this.$msg.success(this.$t('task.remove-record-success', {
taskName: this.taskName
}))
})
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.remove-record-fail', {
taskName: this.taskName
}))
}
})
},
onResumeClick () {
this.$store.dispatch('task/resumeTask', this.task)
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.resume-task-fail', {
taskName: this.taskName
}))
}
})
const { task, taskName } = this
commands.emit('resume-task', {
task,
taskName
})
},
onRestartClick (event) {
const { task, taskName } = this
const { gid, status } = task
const uri = getTaskUri(task)
const isNeedShowDialog = status === TASK_STATUS.COMPLETE || !!event.altKey
this.$store.dispatch('task/getTaskOption', gid)
.then((data) => {
console.log('[Motrix] get task option:', data)
const { dir, header, maxConnectionPerServer } = data
const options = {
dir,
header,
maxConnectionPerServer,
out: taskName
}
if (isNeedShowDialog) {
this.showAddTaskDialog(uri, options)
} else {
this.directAddTask(uri, options)
this.$store.dispatch('task/removeTaskRecord', task)
}
})
},
directAddTask (uri, options = {}) {
const uris = [uri]
const payload = {
uris,
options: {
...options
}
}
this.$store.dispatch('task/addUri', payload)
.catch((err) => {
this.$msg.error(err.message)
})
},
showAddTaskDialog (uri, options = {}) {
const {
header,
...rest
} = options
const headers = parseHeader(header)
const newOptions = {
...rest,
...headers
}
this.$store.dispatch('app/updateAddTaskUrl', uri)
this.$store.dispatch('app/updateAddTaskOptions', newOptions)
this.$store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.URI)
const { status } = task
const showDialog = status === TASK_STATUS.COMPLETE || !!event.altKey
commands.emit('restart-task', {
task,
taskName,
showDialog
})
},
onPauseClick () {
this.pauseTask()
const { task, taskName } = this
commands.emit('pause-task', {
task,
taskName
})
},
onStopClick () {
this.stopSeeding()
},
stopSeeding () {
if (!this.isSeeder) {
return
}
this.$store.dispatch('task/stopSeeding', this.task)
},
pauseTask () {
const { taskName } = this
this.$msg.info(this.$t('task.download-pause-message', { taskName }))
this.$store.dispatch('task/pauseTask', this.task)
.catch(({ code }) => {
if (code === 1) {
this.$msg.error(this.$t('task.pause-task-fail', { taskName }))
}
})
const { task } = this
commands.emit('stop-task-seeding', { task })
},
onDeleteClick (event) {
const self = this
const { task } = this
const isChecked = !!event.shiftKey
this.$electron.remote.dialog.showMessageBox({
type: 'warning',
title: this.$t('task.delete-task'),
message: this.$t('task.delete-task-confirm', { taskName: this.taskName }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.delete-task-label'),
checkboxChecked: isChecked
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
self.removeTask(task, checkboxChecked)
}
const { task, taskName } = this
const deleteWithFiles = !!event.shiftKey
commands.emit('delete-task', {
task,
taskName,
deleteWithFiles
})
},
onTrashClick (event) {
const self = this
const { task } = this
const isChecked = !!event.shiftKey
this.$electron.remote.dialog.showMessageBox({
type: 'warning',
title: this.$t('task.remove-record'),
message: this.$t('task.remove-record-confirm', { taskName: this.taskName }),
buttons: [this.$t('app.yes'), this.$t('app.no')],
cancelId: 1,
checkboxLabel: this.$t('task.remove-record-label'),
checkboxChecked: isChecked
}).then(({ response, checkboxChecked }) => {
if (response === 0) {
self.removeTaskRecord(task, checkboxChecked)
}
const { task, taskName } = this
const deleteWithFiles = !!event.shiftKey
commands.emit('delete-task-record', {
task,
taskName,
deleteWithFiles
})
},
onFolderClick () {
showItemInFolder(this.path, {
errorMsg: this.$t('task.file-not-exist')
})
const { path } = this
commands.emit('reveal-in-folder', { path })
},
onLinkClick () {
this.$store.dispatch('app/fetchEngineOptions')
.then((data) => {
const { btTracker } = data
const uri = getTaskUri(this.task, btTracker)
clipboard.writeText(uri)
.then(() => {
this.$msg.success(this.$t('task.copy-link-success'))
})
})
const { task } = this
commands.emit('copy-task-link', { task })
},
onInfoClick () {
this.$store.dispatch('task/showTaskItemInfoDialog', this.task)
const { task } = this
commands.emit('show-task-info', { task })
},
onMoreClick () {
}
@@ -325,33 +195,36 @@
</script>
<style lang="scss">
.task-item-actions {
// width: 28px;
height: 24px;
padding: 0 10px;
margin: 0;
overflow: hidden;
user-select: none;
cursor: default;
text-align: right;
direction: rtl;
border: 1px solid $--task-item-action-border-color;
color: $--task-item-action-color;
background-color: $--task-item-action-background;
border-radius: 14px;
transition: $--all-transition;
&:hover {
border-color: $--task-item-action-hover-border-color;
color: $--task-item-action-hover-color;
background-color: $--task-item-action-hover-background;
width: auto;
}
&> .task-item-action {
.task-item-actions {
// width: 28px;
height: 24px;
padding: 0 10px;
margin: 0;
overflow: hidden;
user-select: none;
cursor: default;
text-align: right;
direction: rtl;
border: 1px solid $--task-item-action-border-color;
color: $--task-item-action-color;
background-color: $--task-item-action-background;
border-radius: 14px;
transition: $--all-transition;
&:hover {
border-color: $--task-item-action-hover-border-color;
color: $--task-item-action-hover-color;
background-color: $--task-item-action-hover-background;
width: auto;
}
&> .task-item-action {
display: inline-block;
padding: 5px;
margin: 0 4px;
font-size: 0;
cursor: pointer;
i {
display: inline-block;
padding: 5px;
margin: 0 4px;
font-size: 0;
cursor: pointer;
}
}
}
</style>
@@ -1,153 +0,0 @@
<template>
<el-dialog
custom-class="task-info-dialog"
width="61.8vw"
v-if="task"
:title="dialogTitle"
:show-close="true"
:visible.sync="visible"
:before-close="handleClose"
@closed="handleClosed"
>
<div class="task-name" :title="taskFullName">
<span>{{ taskFullName }}</span>
</div>
<mo-task-item-actions mode="ITEM" :task="task" />
<div class="task-progress">
<mo-task-progress
:completed="Number(task.completedLength)"
:total="Number(task.totalLength)"
:status="task.status"
/>
<mo-task-progress-info :task="task" />
</div>
</el-dialog>
</template>
<script>
import { mapActions } from 'vuex'
import { getTaskName } from '@shared/utils'
import TaskItemActions from './TaskItemActions'
import TaskProgress from './TaskProgress'
import TaskProgressInfo from './TaskProgressInfo'
import '@/components/Icons/task-start-line'
import '@/components/Icons/task-pause-line'
import '@/components/Icons/delete'
import '@/components/Icons/folder'
import '@/components/Icons/link'
import '@/components/Icons/more'
export default {
name: 'mo-task-item-info',
components: {
[TaskItemActions.name]: TaskItemActions,
[TaskProgress.name]: TaskProgress,
[TaskProgressInfo.name]: TaskProgressInfo
},
props: {
task: {
type: Object
},
visible: {
type: Boolean,
default: false
}
},
computed: {
taskFullName: function () {
return getTaskName(this.task, {
defaultName: this.$t('task.get-task-name'),
maxLen: -1
})
},
taskName: function () {
return getTaskName(this.task, {
defaultName: this.$t('task.get-task-name'),
maxLen: 32
})
},
dialogTitle: function () {
return this.$t('task.task-info-dialog-title', { title: this.taskName })
}
},
methods: {
handleClose (done) {
this.$store.dispatch('task/hideTaskItemInfoDialog')
},
handleClosed (done) {
this.$store.dispatch('task/updateCurrentTaskItem', null)
},
...mapActions('task', [
'toggleTask'
])
}
}
</script>
<style lang="scss">
.task-info-dialog {
min-width: 380px;
.el-dialog__header {
padding-right: 60px;
}
.el-dialog__body {
position: relative;
}
.task-name {
font-size: 14px;
color: #505753;
line-height: 26px;
margin-bottom: 32px;
margin-right: 200px;
}
}
.task-item-info {
padding: 16px 12px;
}
.task-item-actions {
display: inline-block;
position: absolute;
top: 30px;
right: 20px;
}
.task-name {
color: #505753;
margin-bottom: 32px;
margin-right: 240px;
word-break: break-all;
min-height: 26px;
&> span {
font-size: 14px;
line-height: 26px;
overflow : hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.task-progress-info {
font-size: 12px;
line-height: 14px;
min-height: 14px;
color: #9B9B9B;
margin-top: 8px;
}
.task-progress-info-left {
min-height: 14px;
text-align: left;
}
.task-progress-info-right {
min-height: 14px;
text-align: right;
}
.task-speed-info {
& > .task-speed-text {
margin-left: 8px;
& > i {
vertical-align: middle;
}
}
}
</style>
@@ -1,37 +1,44 @@
<template>
<el-row class="task-progress-info">
<el-col :span="8" class="task-progress-info-left">
<div v-if="task.totalLength > 0">
{{ task.completedLength | bytesToSize }} / {{ task.totalLength | bytesToSize }}
<el-col :span="6" class="task-progress-info-left">
<div v-if="task.completedLength > 0 || task.totalLength > 0">
<span>{{ task.completedLength | bytesToSize }}</span>
<span v-if="task.totalLength > 0"> / {{ task.totalLength | bytesToSize }}</span>
</div>
</el-col>
<el-col :span="16" class="task-progress-info-right">
<el-col :span="18" class="task-progress-info-right">
<div class="task-speed-info" v-if="isActive">
<span class="task-speed-text" v-if="isBT">
<i><mo-icon name="arrow-up" width="10" height="10" /></i>
<i>{{ task.uploadSpeed | bytesToSize }}/s</i>
</span>
<span class="task-speed-text">
<i><mo-icon name="arrow-down" width="10" height="10" /></i>
<i>{{ task.downloadSpeed | bytesToSize }}/s</i>
</span>
<span class="task-speed-text" v-if="remaining > 0">
{{
remaining | timeFormat({
prefix: $t('task.remaining-prefix'),
i18n: {
'gt1d': $t('app.gt1d'),
'hour': $t('app.hour'),
'minute': $t('app.minute'),
'second': $t('app.second')
}
})
}}
</span>
<span class="task-speed-text">
<i><mo-icon name="node" width="10" height="10" /></i>
<i>{{ task.connections }}</i>
</span>
<div class="task-speed-text" v-if="isBT">
<i><mo-icon name="arrow-up" width="10" height="14" /></i>
<span>{{ task.uploadSpeed | bytesToSize }}/s</span>
</div>
<div class="task-speed-text">
<i><mo-icon name="arrow-down" width="10" height="14" /></i>
<span>{{ task.downloadSpeed | bytesToSize }}/s</span>
</div>
<div class="task-speed-text" v-if="remaining > 0">
<span>
{{
remaining | timeFormat({
prefix: $t('task.remaining-prefix'),
i18n: {
'gt1d': $t('app.gt1d'),
'hour': $t('app.hour'),
'minute': $t('app.minute'),
'second': $t('app.second')
}
})
}}
</span>
</div>
<div class="task-speed-text" v-if="isBT">
<i><mo-icon name="magnet" width="10" height="14" /></i>
<span>{{ task.numSeeders }}</span>
</div>
<div class="task-speed-text">
<i><mo-icon name="node" width="10" height="14" /></i>
<span>{{ task.connections }}</span>
</div>
</div>
</el-col>
</el-row>
@@ -49,6 +56,7 @@
import '@/components/Icons/arrow-up'
import '@/components/Icons/arrow-down'
import '@/components/Icons/node'
import '@/components/Icons/magnet'
export default {
name: 'mo-task-progress-info',
@@ -80,27 +88,47 @@
</script>
<style lang="scss">
.task-progress-info {
font-size: 12px;
line-height: 14px;
min-height: 14px;
color: #9B9B9B;
margin-top: 8px;
.task-progress-info {
font-size: 0.75rem;
line-height: 0.875rem;
min-height: 0.875rem;
color: #9B9B9B;
margin-top: 0.5rem;
i {
font-style: normal;
}
.task-progress-info-left {
min-height: 14px;
text-align: left;
}
.task-progress-info-right {
min-height: 14px;
text-align: right;
}
.task-speed-info {
& > .task-speed-text {
margin-left: 8px;
& > i {
vertical-align: middle;
}
}
.task-progress-info-left {
min-height: 0.875rem;
text-align: left;
}
.task-progress-info-right {
min-height: 0.875rem;
text-align: right;
}
.task-speed-info {
font-size: 0;
& > .task-speed-text {
margin-left: 0.375rem;
font-size: 0;
line-height: 0.875rem;
vertical-align: middle;
display: inline-block;
&:first-of-type {
margin-left: 0;
}
& > i, & > span {
height: 0.875rem;
line-height: 0.875rem;
display: inline-block;
vertical-align: middle;
}
& > i {
margin-right: 0.125rem;
}
& > span {
font-size: 0.75rem;
}
}
}
</style>
@@ -0,0 +1,45 @@
<template>
<el-tag :effect="theme" class="tag-task-status" :type="type">
{{ status && status.toUpperCase() }}
</el-tag>
</template>
<script>
import { APP_THEME, TASK_STATUS } from '@shared/constants'
import colors from '@shared/colors'
const statusTypeMap = {
[TASK_STATUS.ACTIVE]: 'success',
[TASK_STATUS.WAITING]: 'info',
[TASK_STATUS.PAUSED]: 'info',
[TASK_STATUS.ERROR]: 'danger',
[TASK_STATUS.COMPLETE]: 'success',
[TASK_STATUS.REMOVED]: 'info',
[TASK_STATUS.SEEDING]: 'success'
}
export default {
name: 'mo-task-status',
props: {
theme: {
type: String,
default: APP_THEME.DARK,
validator: function (value) {
return [APP_THEME.LIGHT, APP_THEME.DARK].indexOf(value) !== -1
}
},
status: {
type: String,
default: TASK_STATUS.ACTIVE
}
},
computed: {
type () {
return statusTypeMap[this.status]
},
color () {
return colors[this.status]
}
}
}
</script>
@@ -0,0 +1,268 @@
<template>
<el-drawer
custom-class="panel task-detail-drawer"
size="61.8%"
v-if="gid"
:title="$t('task.task-detail-title')"
:with-header="true"
:show-close="true"
:visible.sync="visible"
:before-close="handleClose"
@closed="handleClosed"
>
<el-tabs
tab-position="top"
class="task-detail-tab"
value="general"
:before-leave="handleTabBeforeLeave"
@tab-click="handleTabClick"
>
<el-tab-pane name="general">
<span class="task-detail-tab-label" slot="label"><i class="el-icon-info"></i></span>
<mo-task-general :task="task" />
</el-tab-pane>
<el-tab-pane name="activity" lazy>
<span class="task-detail-tab-label" slot="label"><i class="el-icon-s-grid"></i></span>
<mo-task-activity ref="taskGraphic" :task="task" />
</el-tab-pane>
<el-tab-pane name="trackers" lazy v-if="isBT">
<span class="task-detail-tab-label" slot="label"><i class="el-icon-discover"></i></span>
<mo-task-trackers :task="task" />
</el-tab-pane>
<el-tab-pane name="peers" lazy v-if="isBT">
<span class="task-detail-tab-label" slot="label"><i class="el-icon-s-custom"></i></span>
<mo-task-peers :peers="peers" />
</el-tab-pane>
<el-tab-pane name="files" lazy>
<span class="task-detail-tab-label" slot="label"><i class="el-icon-files"></i></span>
<mo-task-files
ref="detailFileList"
mode="DETAIL"
:files="fileList"
@selection-change="handleSelectionChange"
/>
</el-tab-pane>
</el-tabs>
<div class="task-detail-actions">
<mo-task-item-actions mode="DETAIL" :task="task" />
</div>
</el-drawer>
</template>
<script>
import is from 'electron-is'
import { debounce, merge } from 'lodash'
import {
calcFormLabelWidth,
checkTaskIsBT,
checkTaskIsSeeder,
getFileName,
getFileExtension
} from '@shared/utils'
import { EMPTY_STRING, TASK_STATUS } from '@shared/constants'
import TaskItemActions from '@/components/Task/TaskItemActions'
import TaskGeneral from './TaskGeneral'
import TaskActivity from './TaskActivity'
import TaskTrackers from './TaskTrackers'
import TaskPeers from './TaskPeers'
import TaskFiles from './TaskFiles'
const cached = {
files: []
}
export default {
name: 'mo-task-detail',
components: {
[TaskItemActions.name]: TaskItemActions,
[TaskGeneral.name]: TaskGeneral,
[TaskActivity.name]: TaskActivity,
[TaskTrackers.name]: TaskTrackers,
[TaskPeers.name]: TaskPeers,
[TaskFiles.name]: TaskFiles
},
props: {
gid: {
type: String
},
task: {
type: Object
},
files: {
type: Array,
default: function () {
return []
}
},
peers: {
type: Array,
default: function () {
return []
}
},
visible: {
type: Boolean,
default: false
}
},
data () {
const { locale } = this.$store.state.preference.config
return {
form: {},
formLabelWidth: calcFormLabelWidth(locale),
locale,
activeTab: 'general',
graphicWidth: 0
}
},
computed: {
isRenderer: () => is.renderer(),
isBT () {
return checkTaskIsBT(this.task)
},
isSeeder () {
return checkTaskIsSeeder(this.task)
},
taskStatus () {
const { task, isSeeder } = this
if (isSeeder) {
return TASK_STATUS.SEEDING
} else {
return task.status
}
},
fileList () {
const { files } = this
const result = files.map((item) => {
const name = getFileName(item.path)
const extension = getFileExtension(name)
return {
idx: Number(item.index),
selected: item.selected === 'true',
path: item.path,
name,
extension,
length: item.length,
completedLength: item.completedLength
}
})
merge(cached.files, result)
return cached.files
},
selectedFileList () {
const { fileList } = this
const result = fileList.filter((item) => item.selected)
return result
}
},
mounted () {
window.addEventListener('resize', debounce(() => {
console.log('resize===>', this.activeTab, this.$refs.taskGraphic)
if (this.activeTab === 'activity' && this.$refs.taskGraphic) {
this.$refs.taskGraphic.updateGraphicWidth()
}
}, 300))
},
destroyed () {
cached.files = []
window.removeEventListener('resize')
},
methods: {
handleClose (done) {
this.$store.dispatch('task/hideTaskDetail')
},
handleClosed (done) {
this.$store.dispatch('task/updateCurrentTaskGid', EMPTY_STRING)
this.$store.dispatch('task/updateCurrentTaskItem', null)
},
handleTabBeforeLeave (activeName, oldActiveName) {
this.activeTab = activeName
if (oldActiveName !== 'peers') {
return
}
this.$store.dispatch('task/toggleEnabledFetchPeers', false)
},
handleTabClick (tab) {
const { name } = tab
switch (name) {
case 'peers':
this.$store.dispatch('task/toggleEnabledFetchPeers', true)
break
case 'files':
setImmediate(() => {
this.updateFilesListSelection()
})
break
}
},
updateFilesListSelection () {
if (!this.$refs.detailFileList) {
return
}
const { selectedFileList } = this
this.$refs.detailFileList.toggleSelection(selectedFileList)
},
handleSelectionChange (val) {
console.log('task detail handleSelectionChange==>', val)
}
}
}
</script>
<style lang="scss">
.task-detail-drawer {
.el-drawer__header {
margin-bottom: 0;
}
.el-drawer__body {
position: relative;
}
.task-detail-actions {
position: sticky;
left: 0;
bottom: 1rem;
z-index: inherit;
width: 100%;
text-align: center;
font-size: 0;
.task-item-actions {
display: inline-block;
&> .task-item-action {
margin: 0 0.5rem;
}
}
}
.task-detail-drawer-title {
&> span, &> ul {
vertical-align: middle;
}
}
}
.task-detail-tab {
height: 100%;
padding: 0.5rem 1.25rem 3.125rem;
display: flex;
flex-direction: column;
.task-detail-tab-label {
padding: 0 0.75rem;
}
.el-tabs__content {
position: relative;
height: 100%;
}
.el-tab-pane {
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
</style>
@@ -0,0 +1,217 @@
<template>
<el-form
class="mo-task-activity"
ref="form"
:model="form"
:label-width="formLabelWidth"
v-if="task"
>
<div class="graphic-box" ref="graphicBox">
<mo-task-graphic
:width="graphicWidth"
:bitfield="task.bitfield"
v-if="graphicWidth > 0"
/>
</div>
<el-form-item :label="`${$t('task.task-progress-info')}: `">
<div class="form-static-value" style="overflow: hidden">
<el-row :gutter="12">
<el-col :span="18">
<div class="progress-wrapper">
<mo-task-progress
:completed="Number(task.completedLength)"
:total="Number(task.totalLength)"
:status="task.status"
/>
</div>
</el-col>
<el-col :span="5">
{{ percent }}
</el-col>
</el-row>
</div>
</el-form-item>
<el-form-item>
<div class="form-static-value">
<span>{{ task.completedLength | bytesToSize }}</span>
<span v-if="task.totalLength > 0"> / {{ task.totalLength | bytesToSize }}</span>
<span class="task-time-remaining" v-if="isActive && remaining > 0">
{{
remaining | timeFormat({
prefix: $t('task.remaining-prefix'),
i18n: {
'gt1d': $t('app.gt1d'),
'hour': $t('app.hour'),
'minute': $t('app.minute'),
'second': $t('app.second')
}
})
}}
</span>
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-num-seeders')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.numSeeders }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-connections')}: `">
<div class="form-static-value">
{{ task.connections }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-download-speed')}: `">
<div class="form-static-value">
<span>{{ task.downloadSpeed | bytesToSize }}/s</span>
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-upload-speed')}: `" v-if="isBT">
<div class="form-static-value">
<span>{{ task.uploadSpeed | bytesToSize }}/s</span>
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-upload-length')}: `" v-if="isBT">
<div class="form-static-value">
<span>{{ task.uploadLength | bytesToSize }}</span>
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-ratio')}: `" v-if="isBT">
<div class="form-static-value">
{{ ratio }}
</div>
</el-form-item>
</el-form>
</template>
<script>
import is from 'electron-is'
import {
bytesToSize,
calcFormLabelWidth,
calcProgress,
calcRatio,
checkTaskIsBT,
checkTaskIsSeeder,
timeFormat,
timeRemaining
} from '@shared/utils'
import { TASK_STATUS } from '@shared/constants'
import TaskGraphic from '@/components/TaskGraphic/Index'
import TaskProgress from '@/components/Task/TaskProgress'
export default {
name: 'mo-task-activity',
components: {
[TaskGraphic.name]: TaskGraphic,
[TaskProgress.name]: TaskProgress
},
props: {
gid: {
type: String
},
task: {
type: Object
},
files: {
type: Array,
default: function () {
return []
}
},
peers: {
type: Array,
default: function () {
return []
}
},
visible: {
type: Boolean,
default: false
}
},
data () {
const { locale } = this.$store.state.preference.config
return {
form: {},
formLabelWidth: calcFormLabelWidth(locale),
locale,
graphicWidth: 0
}
},
computed: {
isRenderer: () => is.renderer(),
isBT () {
return checkTaskIsBT(this.task)
},
isSeeder () {
return checkTaskIsSeeder(this.task)
},
taskStatus () {
const { task, isSeeder } = this
if (isSeeder) {
return TASK_STATUS.SEEDING
} else {
return task.status
}
},
isActive () {
return this.taskStatus === TASK_STATUS.ACTIVE
},
percent () {
const { totalLength, completedLength } = this.task
const percent = calcProgress(totalLength, completedLength)
return `${percent}%`
},
remaining () {
const { totalLength, completedLength, downloadSpeed } = this.task
return timeRemaining(totalLength, completedLength, downloadSpeed)
},
ratio () {
if (!this.isBT) {
return 0
}
const { totalLength, uploadLength } = this.task
const ratio = calcRatio(totalLength, uploadLength)
return ratio
}
},
filters: {
bytesToSize,
timeFormat
},
mounted () {
setImmediate(() => {
this.updateGraphicWidth()
})
},
methods: {
updateGraphicWidth () {
if (!this.$refs.graphicBox) {
return
}
console.log('updateGraphicWidth===>', this.$refs.graphicBox)
this.graphicWidth = this.calcInnerWidth(this.$refs.graphicBox)
},
calcInnerWidth (ele) {
if (!ele) {
return 0
}
const style = getComputedStyle(ele, null)
const width = style.getPropertyValue('width')
return parseInt(width, 10)
}
}
}
</script>
<style lang="scss">
.progress-wrapper {
padding: 0.6875rem 0 0 0;
}
.task-time-remaining {
margin-left: 1rem;
}
</style>
@@ -0,0 +1,193 @@
<template>
<div class="mo-task-files" v-if="files">
<div class="mo-table-wrapper">
<el-table
stripe
ref="torrentTable"
:height="height"
:data="files"
tooltip-effect="dark"
style="width: 100%"
@row-dblclick="handleRowDbClick"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="42">
</el-table-column>
<el-table-column
:label="$t('task.file-name')"
min-width="200"
show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>
<el-table-column
:label="$t('task.file-extension')"
width="80">
<template slot-scope="scope">{{ scope.row.extension | removeExtensionDot }}</template>
</el-table-column>
<el-table-column
:label="$t('task.file-size')"
align="right"
width="85">
<template slot-scope="scope">{{ scope.row.length | bytesToSize }}</template>
</el-table-column>
<el-table-column
v-if="mode === 'DETAIL'"
:label="$t('task.file-completed-size')"
align="right"
width="95">
<template slot-scope="scope">{{ scope.row.completedLength | bytesToSize }}</template>
</el-table-column>
</el-table>
</div>
<el-row class="file-filters" :gutter="12" v-if="mode === 'ADD'">
<el-col class="quick-filters" :span="8">
<el-button-group>
<el-button @click="toggleVideoSelection()">
<mo-icon name="video" width="12" height="12" />
</el-button>
<el-button @click="toggleAudioSelection()">
<mo-icon name="audio" width="12" height="12" />
</el-button>
<el-button @click="toggleImageSelection()">
<mo-icon name="image" width="12" height="12" />
</el-button>
</el-button-group>
</el-col>
<el-col :span="16" style="text-align: right">
{{ $t('task.selected-files-sum', { selectedFilesCount, selectedFilesTotalSize }) }}
</el-col>
</el-row>
</div>
</template>
<script>
import { isEmpty } from 'lodash'
import '@/components/Icons/video'
import '@/components/Icons/audio'
import '@/components/Icons/image'
import {
NONE_SELECTED_FILES,
SELECTED_ALL_FILES
} from '@shared/constants'
import {
bytesToSize,
filterVideoFiles,
filterAudioFiles,
filterImageFiles,
removeExtensionDot
} from '@shared/utils'
export default {
name: 'mo-task-files',
filters: {
bytesToSize,
removeExtensionDot
},
props: {
mode: {
type: String,
default: 'ADD',
validator: function (value) {
return ['ADD', 'DETAIL'].indexOf(value) !== -1
}
},
height: {
type: [Number, String]
},
files: {
type: Array,
default: function () {
return []
}
}
},
data () {
return {
selectedFiles: []
}
},
computed: {
selectedFilesCount () {
return this.selectedFiles.length
},
selectedFilesTotalSize () {
const result = this.selectedFiles.reduce((acc, cur) => {
return acc + cur.length
}, 0)
return bytesToSize(result)
},
selectedFileIndex () {
const { files, selectedFiles } = this
if (files.length === 0 || selectedFiles.length === 0) {
return NONE_SELECTED_FILES
}
if (files.length === selectedFiles.length) {
return SELECTED_ALL_FILES
}
const indexArr = this.selectedFiles.map((item) => item.idx)
const result = indexArr.join(',')
return result
}
},
watch: {
selectedFileIndex () {
const { selectedFileIndex } = this
this.$emit('selection-change', selectedFileIndex)
}
},
methods: {
toggleAllSelection () {
if (!this.$refs.torrentTable) {
return
}
this.$refs.torrentTable.toggleAllSelection()
},
clearSelection () {
if (!this.$refs.torrentTable) {
return
}
this.$refs.torrentTable.clearSelection()
},
toggleSelection (rows) {
if (isEmpty(rows)) {
this.$refs.torrentTable.clearSelection()
} else {
this.$refs.torrentTable.clearSelection()
rows.forEach(row => {
this.$refs.torrentTable.toggleRowSelection(row, true)
})
}
},
toggleVideoSelection () {
const filtered = filterVideoFiles(this.files)
this.toggleSelection(filtered)
},
toggleAudioSelection () {
const filtered = filterAudioFiles(this.files)
this.toggleSelection(filtered)
},
toggleImageSelection () {
const filtered = filterImageFiles(this.files)
this.toggleSelection(filtered)
},
handleRowDbClick (row, column, event) {
this.$refs.torrentTable.toggleRowSelection(row)
},
handleSelectionChange (val) {
this.selectedFiles = val
}
}
}
</script>
<style lang="scss">
.file-filters {
margin-top: 0.5rem;
.quick-filters {
button {
font-size: 0;
}
}
}
</style>
@@ -0,0 +1,179 @@
<template>
<el-form
class="mo-task-general"
ref="form"
:model="form"
:label-width="formLabelWidth"
v-if="task"
>
<el-form-item :label="`${$t('task.task-name')}: `">
<div class="form-static-value">
{{ taskFullName }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-dir')}: `">
<el-input placeholder="" readonly v-model="path">
<mo-show-in-folder
slot="append"
v-if="isRenderer"
:path="path"
/>
</el-input>
</el-form-item>
<el-form-item :label="`${$t('task.task-status')}: `">
<div class="form-static-value">
<mo-task-status :theme="currentTheme" :status="taskStatus" />
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-error-info')}: `" v-if="task.errorCode && task.errorCode !== '0'">
<div class="form-static-value">
{{ task.errorCode }} {{ task.errorMessage }}
</div>
</el-form-item>
<el-divider v-if="isBT">
<i class="el-icon-attract"></i>
{{ $t('task.task-bittorrent-info') }}
</el-divider>
<el-form-item :label="`${$t('task.task-info-hash')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.infoHash }}
<i class="copy-link" @click="handleCopyClick">
<mo-icon
name="link"
width="12"
height="12"
/>
</i>
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-piece-length')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.pieceLength | bytesToSize }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-num-pieces')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.numPieces }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-bittorrent-creation-date')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.bittorrent.creationDate | localeDateTimeFormat(locale) }}
</div>
</el-form-item>
<el-form-item :label="`${$t('task.task-bittorrent-comment')}: `" v-if="isBT">
<div class="form-static-value">
{{ task.bittorrent.comment }}
</div>
</el-form-item>
</el-form>
</template>
<script>
import is from 'electron-is'
import { mapState } from 'vuex'
import * as clipboard from 'clipboard-polyfill'
import {
bytesToSize,
calcFormLabelWidth,
checkTaskIsBT,
checkTaskIsSeeder,
getTaskName,
getTaskUri,
localeDateTimeFormat
} from '@shared/utils'
import { APP_THEME, TASK_STATUS } from '@shared/constants'
import { getTaskFullPath } from '@/utils/native'
import ShowInFolder from '@/components/Native/ShowInFolder'
import TaskStatus from '@/components/Task/TaskStatus'
import '@/components/Icons/folder'
import '@/components/Icons/link'
export default {
name: 'mo-task-general',
components: {
[ShowInFolder.name]: ShowInFolder,
[TaskStatus.name]: TaskStatus
},
props: {
task: {
type: Object
}
},
data () {
const { locale } = this.$store.state.preference.config
return {
form: {},
formLabelWidth: calcFormLabelWidth(locale),
locale
}
},
computed: {
...mapState('app', {
systemTheme: state => state.systemTheme
}),
...mapState('preference', {
theme: state => state.config.theme
}),
currentTheme () {
if (this.theme === APP_THEME.AUTO) {
return this.systemTheme
} else {
return this.theme
}
},
isRenderer: () => is.renderer(),
taskFullName () {
return getTaskName(this.task, {
defaultName: this.$t('task.get-task-name'),
maxLen: -1
})
},
taskName () {
return getTaskName(this.task, {
defaultName: this.$t('task.get-task-name'),
maxLen: 32
})
},
isSeeder () {
return checkTaskIsSeeder(this.task)
},
taskStatus () {
const { task, isSeeder } = this
if (isSeeder) {
return TASK_STATUS.SEEDING
} else {
return task.status
}
},
path () {
return getTaskFullPath(this.task)
},
isBT () {
return checkTaskIsBT(this.task)
}
},
filters: {
bytesToSize,
localeDateTimeFormat
},
methods: {
handleCopyClick () {
const { task } = this
const uri = getTaskUri(task)
clipboard.writeText(uri)
.then(() => {
this.$msg.success(this.$t('task.copy-link-success'))
})
}
}
}
</script>
<style lang="scss">
.copy-link {
cursor: pointer;
}
</style>
@@ -0,0 +1,83 @@
<template>
<div class="mo-task-peers">
<div class="mo-table-wrapper">
<el-table
stripe
ref="peerTable"
class="mo-peer-table"
:data="peers"
>
<el-table-column
:label="`${$t('task.task-peer-host')}: `"
min-width="140">
<template slot-scope="scope">
{{ `${scope.row.ip}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column
:label="`${$t('task.task-peer-client')}: `"
min-width="125">
<template slot-scope="scope">
{{ scope.row.peerId | peerIdParser }}
</template>
</el-table-column>
<el-table-column
:label="`%`"
align="right"
width="55">
<template slot-scope="scope">
{{ scope.row.bitfield | bitfieldToPercent }}%
</template>
</el-table-column>
<el-table-column
:label="`↑`"
align="right"
width="90">
<template slot-scope="scope">
{{ scope.row.uploadSpeed | bytesToSize }}/s
</template>
</el-table-column>
<el-table-column
:label="`↓`"
align="right"
width="90">
<template slot-scope="scope">
{{ scope.row.downloadSpeed | bytesToSize }}/s
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import {
bitfieldToPercent,
bytesToSize,
peerIdParser
} from '@shared/utils'
export default {
name: 'mo-task-peers',
filters: {
bitfieldToPercent,
bytesToSize,
peerIdParser
},
props: {
peers: {
type: Array,
default: function () {
return []
}
}
}
}
</script>
<style lang="scss">
.el-table.mo-peer-table .cell {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
</style>
@@ -0,0 +1,80 @@
<template>
<el-form
ref="form"
:model="form"
:label-width="formLabelWidth"
v-if="task"
>
<div
class="tracker-list"
v-if="announceList"
>
<el-input
readonly
autosize
type="textarea"
auto-complete="off"
v-model="announceList">
</el-input>
</div>
</el-form>
</template>
<script>
import is from 'electron-is'
import {
calcFormLabelWidth,
checkTaskIsBT,
checkTaskIsSeeder
} from '@shared/utils'
import { convertTrackerDataToLine } from '@shared/utils/tracker'
import { EMPTY_STRING } from '@shared/constants'
export default {
name: 'mo-task-trackers',
props: {
task: {
type: Object
}
},
data () {
const { locale } = this.$store.state.preference.config
return {
form: {},
formLabelWidth: calcFormLabelWidth(locale),
locale
}
},
computed: {
isRenderer: () => is.renderer(),
isBT () {
return checkTaskIsBT(this.task)
},
isSeeder () {
return checkTaskIsSeeder(this.task)
},
announceList () {
if (!this.isBT) {
return EMPTY_STRING
}
const { bittorrent } = this.task
const data = bittorrent.announceList.map((i) => i[0])
return convertTrackerDataToLine(data)
}
},
methods: {
}
}
</script>
<style lang="scss">
.tracker-list {
padding: 0;
margin: 0;
font-size: $--font-size-small;
textarea {
line-height: 2;
}
}
</style>
@@ -0,0 +1,75 @@
<template>
<rect
:class="klass"
:status="status"
:width="width"
:height="height"
:rx="radius"
:ry="radius"
:x="x"
:y="y"
>
</rect>
</template>
<script>
export default {
name: 'mo-task-graphic-atom',
props: {
status: {
type: Number
},
width: {
type: Number,
default: 10
},
height: {
type: Number,
default: 10
},
radius: {
type: Number,
default: 2
},
x: {
type: Number
},
y: {
type: Number
}
},
computed: {
klass () {
const { status } = this
return `graphic-atom graphic-atom-s${status}`
}
}
}
</script>
<style lang="scss">
.graphic-atom {
shape-rendering: geometricPrecision;
outline-offset: -1px;
}
.graphic-atom-s0 {
fill: $--graphic-atom-color-0;
outline: 1px solid $--graphic-atom-outline-color;
}
.graphic-atom-s1 {
fill: $--graphic-atom-color-1;
outline: 1px solid $--graphic-atom-outline-color;
}
.graphic-atom-s2 {
fill: $--graphic-atom-color-2;
outline: 1px solid $--graphic-atom-outline-color;
}
.graphic-atom-s3 {
fill: $--graphic-atom-color-3;
outline: 1px solid $--graphic-atom-outline-color;
}
.graphic-atom-s4 {
fill: $--graphic-atom-color-4;
outline: 1px solid $--graphic-atom-outline-color;
}
</style>
@@ -0,0 +1,127 @@
<template>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
class="svg-task-graphic"
:width="width"
:height="height"
:viewBox="box">
<g v-for="(row, index) in atoms" :key="`g-${index}`" >
<mo-task-graphic-atom
v-for="atom in row"
:key="`atom-${atom.id}`"
:status="atom.status"
:width="atomWidth"
:height="atomHeight"
:radius="atomRadius"
:x="atom.x"
:y="atom.y"
/>
</g>
</svg>
</template>
<script>
import Atom from './Atom'
export default {
name: 'mo-task-graphic',
components: {
[Atom.name]: Atom
},
props: {
bitfield: {
type: String,
default: ''
},
width: {
type: Number,
default: 240
},
atomWidth: {
type: Number,
default: 10
},
atomHeight: {
type: Number,
default: 10
},
atomGutter: {
type: Number,
default: 3
},
atomRadius: {
type: Number,
default: 2
}
},
computed: {
len () {
return this.bitfield.length
},
atomWG () {
return this.atomWidth + this.atomGutter
},
atomHG () {
return this.atomHeight + this.atomGutter
},
columnCount () {
const { width, atomWidth, atomWG } = this
const result = parseInt((width - atomWidth) / atomWG, 10) + 1
return result
},
rowCount () {
const { len, columnCount } = this
const result = parseInt((len / columnCount), 10) + 1
return result
},
offset () {
const { width, atomWidth, atomWG, columnCount } = this
const totalWidth = atomWG * (columnCount - 1) + atomWidth
const result = (width - totalWidth) / 2
return parseInt(result, 10)
},
height () {
const { atomHeight, atomHG, rowCount, offset } = this
const result = atomHG * (rowCount - 1) + atomHeight + offset * 2
return parseInt(result, 10)
},
box () {
return `0 0 ${this.width} ${this.height}`
},
atoms () {
const { len, columnCount } = this
const result = []
let row = []
for (let i = 0; i < len; i++) {
row.push(this.buildAtom(i))
if ((i + 1) % columnCount === 0) {
result.push(row)
row = []
}
}
result.push(row)
return result
}
},
methods: {
buildAtom (index) {
const { bitfield, offset, atomWG, atomHG, columnCount } = this
const hIndex = index + 1
let chIndex = index % columnCount
let rhIndex = parseInt((index / columnCount), 10)
chIndex = chIndex < 0 ? 0 : chIndex
rhIndex = rhIndex < 0 ? 0 : rhIndex
const result = {
id: `${hIndex}`,
status: Math.floor(parseInt(bitfield[index], 16) / 4),
x: offset + chIndex * atomWG,
y: offset + rhIndex * atomHG
}
return result
}
}
}
</script>
+45 -1
View File
@@ -59,6 +59,7 @@
.form-actions {
background: $--dk-form-actions-background;
}
.panel {
background-color: $--dk-panel-background;
}
@@ -98,13 +99,46 @@
.mo-speedometer {
background-color: $--dk-speedometer-background;
border-color: #5f5f5f;
border-color: $--dk-speedometer-border-color;
}
.no-task {
color: $--dk-no-task-color;
}
.mo-table-wrapper {
border-color: $--dk-table-border-color;
}
.graphic-box {
border-color: $--dk-task-detail-box-border;
background-color: $--dk-graphic-box-background;
}
.graphic-atom-s0 {
fill: $--dk-graphic-atom-color-0;
}
.graphic-atom-s1 {
fill: $--dk-graphic-atom-color-1;
}
.graphic-atom-s2 {
fill: $--dk-graphic-atom-color-2;
}
.graphic-atom-s3 {
fill: $--dk-graphic-atom-color-3;
}
.graphic-atom-s4 {
fill: $--dk-graphic-atom-color-4;
}
.form-static-value {
color: #e7e7e7;
}
/* Element UI
-------------------------- */
.el-progress-bar__outer {
@@ -390,4 +424,14 @@
}
}
}
/* Divider */
.el-divider {
background-color: #666;
}
.el-divider__text {
background-color: $--dk-panel-background;
color: #a7a7a7;
}
}
@@ -53,6 +53,7 @@ $--dk-task-item-action-color: #eee !default;
$--dk-task-item-action-hover-color: #fff !default;
$--dk-no-task-color: #aaa !default;
$--dk-add-task-dialog-footer-background: #4a4a4a !default;
$--dk-task-detail-box-border: #565656 !default;
/* Preference
-------------------------- */
@@ -61,12 +62,21 @@ $--dk-preference-form-text-color: #dfdfdf !default;
/* Speedometer
-------------------------- */
$--dk-speedometer-background: #333 !default;
$--dk-speedometer-border-color: #ccc !default;
$--dk-speedometer-border-color: #5f5f5f !default;
$--dk-speedometer-hover-border-color: #9b9b9b !default;
$--dk-speedometer-primary-color: $--color-primary !default;
$--dk-speedometer-stopped-color: #9b9b9b !default;
$--dk-speedometer-text-color: #9b9b9b !default;
/* Task Graphic
-------------------------- */
$--dk-graphic-box-background: #3f3f3f !default;
$--dk-graphic-atom-color-0: #161b22 !default;
$--dk-graphic-atom-color-1: #0e4429 !default;
$--dk-graphic-atom-color-2: #006d32 !default;
$--dk-graphic-atom-color-3: #26a641 !default;
$--dk-graphic-atom-color-4: #39d353 !default;
/* Element UI
-------------------------- */
$--dk-dialog-background: #343434 !default;
@@ -133,6 +133,10 @@ iframe {
}
}
.el-drawer__container {
outline: none;
}
/* App Main
-------------------------- */
#app,
@@ -250,6 +254,31 @@ iframe {
background: $--form-actions-background;
}
.mo-table-wrapper {
border: 1px solid $--table-border-color;
border-bottom: none;
overflow-x: hidden;
overflow-y: auto;
.el-table th {
padding: 2px 0;
}
}
.graphic-box {
padding: 0.5rem;
margin-bottom: 1.5rem;
font-size: 0;
line-height: 0;
border: 1px solid $--task-detail-box-border;
border-radius: $--border-radius-base;
box-sizing: content-box;
}
.form-static-value {
word-break: break-all;
color: $--input-font-color;
}
@media only screen and (max-width:567px) {
.hidden-xs-only {
display:none!important
@@ -54,6 +54,7 @@ $--task-item-action-color: #9B9B9B !default;
$--task-item-action-hover-color: #fff !default;
$--no-task-color: #eee !default;
$--add-task-dialog-footer-background: #f5f5f5 !default;
$--task-detail-box-border: #ebeef5 !default;
/* Preference
-------------------------- */
@@ -68,6 +69,17 @@ $--speedometer-primary-color: $--color-primary !default;
$--speedometer-stopped-color: #9b9b9b !default;
$--speedometer-text-color: #9b9b9b !default;
/* Task Graphic
-------------------------- */
$--graphic-box-background: transparent !default;
$--graphic-atom-outline-color: rgba(27, 31, 35, 0.06) !default;
$--graphic-atom-color-0: #ebedf0 !default;
$--graphic-atom-color-1: #9be9a8 !default;
$--graphic-atom-color-2: #40c463 !default;
$--graphic-atom-color-3: #30a14e !default;
$--graphic-atom-color-4: #39d353 !default;
/* Element UI
-------------------------- */
$--dialog-background: #fff !default;
$--table-border-color: #ebeef5 !default;
+21 -12
View File
@@ -9,6 +9,7 @@
:secret="rpcSecret"
/>
<mo-ipc v-if="isRenderer" />
<mo-dynamic-tray v-if="enableTraySpeedometer" />
</div>
</template>
@@ -17,19 +18,22 @@
import { mapState } from 'vuex'
import { getLangDirection } from '@shared/utils'
import { APP_THEME } from '@shared/constants'
import TitleBar from '@/components/Native/TitleBar'
import DynamicTray from '@/components/Native/DynamicTray'
import EngineClient from '@/components/Native/EngineClient'
import Ipc from '@/components/Native/Ipc'
import TitleBar from '@/components/Native/TitleBar'
export default {
name: 'Motrix',
components: {
[TitleBar.name]: TitleBar,
[DynamicTray.name]: DynamicTray,
[EngineClient.name]: EngineClient,
[Ipc.name]: Ipc
[Ipc.name]: Ipc,
[TitleBar.name]: TitleBar
},
computed: {
isRenderer () { return is.renderer() },
isMac: () => is.macOS(),
isRenderer: () => is.renderer(),
...mapState('app', {
systemTheme: state => state.systemTheme
}),
@@ -37,43 +41,48 @@
showWindowActions: state => {
return (is.windows() || is.linux()) && state.config.hideAppMenu
},
traySpeedometer: state => state.config.traySpeedometer,
rpcSecret: state => state.config.rpcSecret,
theme: state => state.config.theme,
locale: state => state.config.locale,
dir: state => getLangDirection(state.config.locale)
}),
themeClass: function () {
themeClass () {
if (this.theme === APP_THEME.AUTO) {
return `theme-${this.systemTheme}`
} else {
return `theme-${this.theme}`
}
},
i18nClass: function () {
i18nClass () {
return `i18n-${this.locale}`
},
dirClass: function () {
dirClass () {
return `dir-${this.dir}`
},
enableTraySpeedometer () {
const { traySpeedometer, isMac, isRenderer } = this
return traySpeedometer && isMac && isRenderer
}
},
methods: {
updateRootClassName: function () {
updateRootClassName () {
const { themeClass = '', i18nClass = '', dirClass = '' } = this
const className = `${themeClass} ${i18nClass} ${dirClass}`
document.documentElement.className = className
}
},
beforeMount: function () {
beforeMount () {
this.updateRootClassName()
},
watch: {
themeClass: function (val, oldVal) {
themeClass (val, oldVal) {
this.updateRootClassName()
},
i18nClass: function (val, oldVal) {
i18nClass (val, oldVal) {
this.updateRootClassName()
},
dirClass: function (val, oldVal) {
dirClass (val, oldVal) {
this.updateRootClassName()
}
}
+186
View File
@@ -0,0 +1,186 @@
import { Message } from 'element-ui'
import { base64StringToBlob } from 'blob-util'
import router from '@/router'
import store from '@/store'
import { buildFileList } from '@shared/utils'
import { ADD_TASK_TYPE } from '@shared/constants'
import { getLocaleManager } from '@/components/Locale'
import { commands } from '@/components/CommandManager/instance'
import {
initTaskForm,
buildUriPayload,
buildTorrentPayload
} from '@/utils/task'
const i18n = getLocaleManager().getI18n()
const updateSystemTheme = (payload = {}) => {
const { theme } = payload
store.dispatch('app/updateSystemTheme', theme)
}
const updateTheme = (payload = {}) => {
const { theme } = payload
store.dispatch('preference/updateThemeConfig', theme)
}
const updateTrayFocused = (payload = {}) => {
const { focused } = payload
store.dispatch('app/updateTrayFocused', focused)
}
const showAboutPanel = () => {
store.dispatch('app/showAboutPanel')
}
const addTask = (payload = {}) => {
const {
type = ADD_TASK_TYPE.URI,
uri,
silent,
...rest
} = payload
const options = {
...rest
}
if (type === ADD_TASK_TYPE.URI && uri) {
store.dispatch('app/updateAddTaskUrl', uri)
}
store.dispatch('app/updateAddTaskOptions', options)
if (silent) {
addTaskSilent(type)
return
}
store.dispatch('app/showAddTaskDialog', type)
}
const addTaskSilent = (type) => {
try {
addTaskByType(type)
} catch (err) {
Message.error(i18n.t(err.message))
}
}
const addTaskByType = (type) => {
const form = initTaskForm(store.state)
let payload = null
if (type === ADD_TASK_TYPE.URI) {
payload = buildUriPayload(form)
store.dispatch('task/addUri', payload).catch(err => {
Message.error(err.message)
})
} else if (type === ADD_TASK_TYPE.TORRENT) {
payload = buildTorrentPayload(form)
store.dispatch('task/addTorrent', payload).catch(err => {
Message.error(err.message)
})
} else if (type === 'metalink') {
// @TODO addMetalink
} else {
console.error('addTask fail', form)
}
}
const showAddBtTask = () => {
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
}
const showAddBtTaskWithFile = (payload = {}) => {
const { name, dataURL = '' } = payload
if (!dataURL) {
return
}
const blob = base64StringToBlob(dataURL, 'application/x-bittorrent')
const file = new File([blob], name, { type: 'application/x-bittorrent' })
const fileList = buildFileList(file)
store.dispatch('app/showAddTaskDialog', ADD_TASK_TYPE.TORRENT)
setTimeout(() => {
store.dispatch('app/addTaskAddTorrents', { fileList })
}, 200)
}
const navigateTaskList = (payload = {}) => {
const { status = 'active' } = payload
router.push({ path: `/task/${status}` }).catch(err => {
console.log(err)
})
}
const navigatePreferences = () => {
router.push({ path: '/preference' }).catch(err => {
console.log(err)
})
}
const showUnderDevelopmentMessage = () => {
Message.info(i18n.t('app.under-development-message'))
}
const pauseTask = () => {
store.dispatch('task/batchPauseSelectedTasks')
}
const resumeTask = () => {
store.dispatch('task/batchResumeSelectedTasks')
}
const deleteTask = () => {
commands.emit('batch-delete-task', {
deleteWithFiles: false
})
}
const moveTaskUp = () => {
showUnderDevelopmentMessage()
}
const moveTaskDown = () => {
showUnderDevelopmentMessage()
}
const pauseAllTask = () => {
store.dispatch('task/pauseAllTask')
}
const resumeAllTask = () => {
store.dispatch('task/resumeAllTask')
}
const selectAllTask = () => {
store.dispatch('task/selectAllTask')
}
const fetchPreference = () => {
store.dispatch('preference/fetchPreference')
}
commands.register('application:task-list', navigateTaskList)
commands.register('application:preferences', navigatePreferences)
commands.register('application:about', showAboutPanel)
commands.register('application:new-task', addTask)
commands.register('application:new-bt-task', showAddBtTask)
commands.register('application:new-bt-task-with-file', showAddBtTaskWithFile)
commands.register('application:pause-task', pauseTask)
commands.register('application:resume-task', resumeTask)
commands.register('application:delete-task', deleteTask)
commands.register('application:move-task-up', moveTaskUp)
commands.register('application:move-task-down', moveTaskDown)
commands.register('application:pause-all-task', pauseAllTask)
commands.register('application:resume-all-task', resumeAllTask)
commands.register('application:select-all-task', selectAllTask)
commands.register('application:update-preference-config', fetchPreference)
commands.register('application:update-system-theme', updateSystemTheme)
commands.register('application:update-theme', updateTheme)
commands.register('application:update-tray-focused', updateTrayFocused)
+46 -2
View File
@@ -1,4 +1,6 @@
import is from 'electron-is'
import { ipcRenderer } from 'electron'
import { getCurrentWindow } from '@electron/remote'
import Vue from 'vue'
import VueI18Next from '@panter/vue-i18next'
import { sync } from 'vuex-router-sync'
@@ -12,9 +14,45 @@ import store from '@/store'
import { getLocaleManager } from '@/components/Locale'
import Icon from '@/components/Icons/Icon'
import Msg from '@/components/Msg'
import { commands } from '@/components/CommandManager/instance'
import TrayWorker from '@/workers/tray.worker'
import '@/components/Theme/Index.scss'
const updateTray = is.renderer()
? async (payload) => {
const { tray } = payload
if (!tray) {
return
}
const ab = await tray.arrayBuffer()
ipcRenderer.send('command', 'application:update-tray', ab)
}
: () => {}
function initTrayWorker () {
const worker = new TrayWorker()
worker.addEventListener('message', (event) => {
const { type, payload } = event.data
switch (type) {
case 'initialized':
case 'log':
console.log('[Motrix] Log from Tray Worker: ', payload)
break
case 'tray:drawed':
updateTray(payload)
break
default:
console.warn('[Motrix] Tray Worker unhandled message type:', type, payload)
}
})
return worker
}
function init (config) {
if (is.renderer()) {
Vue.use(require('vue-electron'))
@@ -36,17 +74,17 @@ function init (config) {
Vue.use(Msg, Message, {
showClose: true
})
Vue.component('mo-icon', Icon)
const loading = Loading.service({
fullscreen: true,
background: 'rgba(0, 0, 0, 0.1)'
})
Vue.component('mo-icon', Icon)
sync(store, router)
/* eslint-disable no-new */
window.app = new Vue({
global.app = new Vue({
components: { App },
router,
store,
@@ -54,8 +92,14 @@ function init (config) {
template: '<App/>'
}).$mount('#app')
global.app.commands = commands
require('./commands')
global.app.trayWorker = initTrayWorker()
setTimeout(() => {
loading.close()
getCurrentWindow().setHasShadow(true)
}, 400)
}
+27 -19
View File
@@ -1,6 +1,6 @@
import { ADD_TASK_TYPE } from '@shared/constants'
import api from '@/api'
import { getSystemTheme } from '@/components/Native/utils'
import { getSystemTheme, isBigSur } from '@/utils/native'
const BASE_INTERVAL = 1000
const PER_INTERVAL = 100
@@ -9,6 +9,8 @@ const MAX_INTERVAL = 6000
const state = {
systemTheme: getSystemTheme(),
bigSur: isBigSur(),
trayFocused: false,
aboutPanelVisible: false,
engineInfo: {
version: '',
@@ -34,10 +36,13 @@ const getters = {
}
const mutations = {
CHANGE_SYSTEM_THEME (state, theme) {
UPDATE_SYSTEM_THEME (state, theme) {
state.systemTheme = theme
},
CHANGE_ABOUT_PANEL_VISIBLE (state, visible) {
UPDATE_TRAY_FOCUSED (state, focused) {
state.trayFocused = focused
},
UPDATE_ABOUT_PANEL_VISIBLE (state, visible) {
state.aboutPanelVisible = visible
},
UPDATE_ENGINE_INFO (state, engineInfo) {
@@ -49,16 +54,16 @@ const mutations = {
UPDATE_GLOBAL_STAT (state, stat) {
state.stat = stat
},
CHANGE_ADD_TASK_VISIBLE (state, visible) {
UPDATE_ADD_TASK_VISIBLE (state, visible) {
state.addTaskVisible = visible
},
CHANGE_ADD_TASK_TYPE (state, taskType) {
UPDATE_ADD_TASK_TYPE (state, taskType) {
state.addTaskType = taskType
},
CHANGE_ADD_TASK_URL (state, text) {
UPDATE_ADD_TASK_URL (state, text) {
state.addTaskUrl = text
},
CHANGE_ADD_TASK_TORRENTS (state, fileList) {
UPDATE_ADD_TASK_TORRENTS (state, fileList) {
state.addTaskTorrents = [...fileList]
},
UPDATE_ADD_TASK_OPTIONS (state, options) {
@@ -93,13 +98,16 @@ const mutations = {
const actions = {
updateSystemTheme ({ commit }, theme) {
commit('CHANGE_SYSTEM_THEME', theme)
commit('UPDATE_SYSTEM_THEME', theme)
},
updateTrayFocused ({ commit }, focused) {
commit('UPDATE_TRAY_FOCUSED', focused)
},
showAboutPanel ({ commit }) {
commit('CHANGE_ABOUT_PANEL_VISIBLE', true)
commit('UPDATE_ABOUT_PANEL_VISIBLE', true)
},
hideAboutPanel ({ commit }) {
commit('CHANGE_ABOUT_PANEL_VISIBLE', false)
commit('UPDATE_ABOUT_PANEL_VISIBLE', false)
},
fetchEngineInfo ({ commit }) {
api.getVersion()
@@ -140,22 +148,22 @@ const actions = {
commit('INCREASE_INTERVAL', millisecond)
},
showAddTaskDialog ({ commit }, taskType) {
commit('CHANGE_ADD_TASK_TYPE', taskType)
commit('CHANGE_ADD_TASK_VISIBLE', true)
commit('UPDATE_ADD_TASK_TYPE', taskType)
commit('UPDATE_ADD_TASK_VISIBLE', true)
},
hideAddTaskDialog ({ commit }) {
commit('CHANGE_ADD_TASK_VISIBLE', false)
commit('CHANGE_ADD_TASK_URL', '')
commit('CHANGE_ADD_TASK_TORRENTS', [])
commit('UPDATE_ADD_TASK_VISIBLE', false)
commit('UPDATE_ADD_TASK_URL', '')
commit('UPDATE_ADD_TASK_TORRENTS', [])
},
changeAddTaskType ({ commit }, taskType) {
commit('CHANGE_ADD_TASK_TYPE', taskType)
commit('UPDATE_ADD_TASK_TYPE', taskType)
},
updateAddTaskUrl ({ commit }, text = '') {
commit('CHANGE_ADD_TASK_URL', text)
updateAddTaskUrl ({ commit }, uri = '') {
commit('UPDATE_ADD_TASK_URL', uri)
},
addTaskAddTorrents ({ commit }, { fileList }) {
commit('CHANGE_ADD_TASK_TORRENTS', fileList)
commit('UPDATE_ADD_TASK_TORRENTS', fileList)
},
updateAddTaskOptions ({ commit }, options = {}) {
commit('UPDATE_ADD_TASK_OPTIONS', options)
+9 -6
View File
@@ -14,26 +14,29 @@ const mutations = {
}
const actions = {
fetchPreference ({ commit }) {
fetchPreference ({ dispatch }) {
return new Promise((resolve) => {
api.fetchPreference()
.then((config) => {
commit('UPDATE_PREFERENCE_DATA', config)
dispatch('updatePreference', config)
resolve(config)
})
})
},
save ({ commit, dispatch }, config) {
save ({ dispatch }, config) {
dispatch('task/saveSession', null, { root: true })
if (isEmpty(config)) {
return
}
commit('UPDATE_PREFERENCE_DATA', config)
dispatch('updatePreference', config)
return api.savePreference(config)
},
changeThemeConfig ({ commit }, theme) {
commit('UPDATE_PREFERENCE_DATA', { theme })
updateThemeConfig ({ dispatch }, theme) {
dispatch('updatePreference', { theme })
},
updatePreference ({ commit }, config) {
commit('UPDATE_PREFERENCE_DATA', config)
},
fetchBtTracker (_, trackerSource = []) {
return fetchBtTrackerFromSource(trackerSource)
+85 -20
View File
@@ -1,11 +1,16 @@
import api from '@/api'
import { TASK_STATUS } from '@shared/constants'
import { intersection } from '@shared/utils'
import { EMPTY_STRING, TASK_STATUS } from '@shared/constants'
import { checkTaskIsBT, intersection } from '@shared/utils'
const state = {
currentList: 'active',
taskItemInfoVisible: false,
taskDetailVisible: false,
currentTaskGid: EMPTY_STRING,
enabledFetchPeers: false,
currentTaskItem: null,
currentTaskFiles: [],
currentTaskPeers: [],
seedingList: [],
taskList: [],
selectedGidList: []
}
@@ -14,6 +19,9 @@ const getters = {
}
const mutations = {
UPDATE_SEEDING_LIST (state, seedingList) {
state.seedingList = seedingList
},
UPDATE_TASK_LIST (state, taskList) {
state.taskList = taskList
},
@@ -23,11 +31,23 @@ const mutations = {
CHANGE_CURRENT_LIST (state, currentList) {
state.currentList = currentList
},
CHANGE_TASK_ITEM_INFO_VISIBLE (state, visible) {
state.taskItemInfoVisible = visible
CHANGE_TASK_DETAIL_VISIBLE (state, visible) {
state.taskDetailVisible = visible
},
UPDATE_CURRENT_TASK_GID (state, gid) {
state.currentTaskGid = gid
},
UPDATE_ENABLED_FETCH_PEERS (state, enabled) {
state.enabledFetchPeers = enabled
},
UPDATE_CURRENT_TASK_ITEM (state, task) {
state.currentTaskItem = task
},
UPDATE_CURRENT_TASK_FILES (state, files) {
state.currentTaskFiles = files
},
UPDATE_CURRENT_TASK_PEERS (state, peers) {
state.currentTaskPeers = peers
}
}
@@ -61,15 +81,36 @@ const actions = {
dispatch('updateCurrentTaskItem', data)
})
},
showTaskItemInfoDialog ({ commit, dispatch }, task) {
dispatch('updateCurrentTaskItem', task)
commit('CHANGE_TASK_ITEM_INFO_VISIBLE', true)
fetchItemWithPeers ({ dispatch }, gid) {
return api.fetchTaskItemWithPeers({ gid })
.then((data) => {
console.log('fetchItemWithPeers===>', data)
dispatch('updateCurrentTaskItem', data)
})
},
hideTaskItemInfoDialog ({ commit }) {
commit('CHANGE_TASK_ITEM_INFO_VISIBLE', false)
showTaskDetail ({ commit, dispatch }, task) {
dispatch('updateCurrentTaskItem', task)
commit('UPDATE_CURRENT_TASK_GID', task.gid)
commit('CHANGE_TASK_DETAIL_VISIBLE', true)
},
hideTaskDetail ({ commit }) {
commit('CHANGE_TASK_DETAIL_VISIBLE', false)
},
toggleEnabledFetchPeers ({ commit }, enabled) {
commit('UPDATE_ENABLED_FETCH_PEERS', enabled)
},
updateCurrentTaskItem ({ commit }, task) {
commit('UPDATE_CURRENT_TASK_ITEM', task)
if (task) {
commit('UPDATE_CURRENT_TASK_FILES', task.files)
commit('UPDATE_CURRENT_TASK_PEERS', task.peers)
} else {
commit('UPDATE_CURRENT_TASK_FILES', [])
commit('UPDATE_CURRENT_TASK_PEERS', [])
}
},
updateCurrentTaskGid ({ commit }, gid) {
commit('UPDATE_CURRENT_TASK_GID', gid)
},
addUri ({ dispatch }, data) {
const { uris, outs, options } = data
@@ -115,25 +156,28 @@ const actions = {
dispatch('saveSession')
})
},
forcePauseTask (_, task) {
forcePauseTask ({ dispatch }, task) {
const { gid, status } = task
if (status !== TASK_STATUS.ACTIVE) {
return Promise.resolve(true)
}
return api.forcePauseTask({ gid })
},
pauseTask ({ dispatch }, task) {
const { gid } = task
return api.pauseTask({ gid })
.catch(() => {
return api.forcePauseTask({ gid })
})
.finally(() => {
dispatch('fetchList')
dispatch('saveSession')
})
},
pauseTask ({ dispatch }, task) {
const { gid } = task
const isBT = checkTaskIsBT(task)
const promise = isBT ? api.forcePauseTask({ gid }) : api.pauseTask({ gid })
promise.finally(() => {
dispatch('fetchList')
dispatch('saveSession')
})
return promise
},
resumeTask ({ dispatch }, task) {
const { gid } = task
return api.resumeTask({ gid })
@@ -159,8 +203,29 @@ const actions = {
dispatch('saveSession')
})
},
stopSeeding ({ dispatch }, task) {
const { gid } = task
addToSeedingList ({ state, commit }, gid) {
const { seedingList } = state
if (seedingList.includes(gid)) {
return
}
const list = [
...seedingList,
gid
]
commit('UPDATE_SEEDING_LIST', list)
},
removeFromSeedingList ({ state, commit }, gid) {
const { seedingList } = state
const idx = seedingList.indexOf(gid)
if (idx === -1) {
return
}
const list = [...seedingList.slice(0, idx), ...seedingList.slice(idx + 1)]
commit('UPDATE_SEEDING_LIST', list)
},
stopSeeding ({ dispatch }, { gid }) {
const options = {
seedTime: 0
}
+141
View File
@@ -0,0 +1,141 @@
import is from 'electron-is'
import { shell, nativeTheme } from '@electron/remote'
import { access, constants } from 'fs'
import { resolve } from 'path'
import { Message } from 'element-ui'
import {
getFileNameFromFile,
isMagnetTask,
getSystemMajorVersion
} from '@shared/utils'
import { APP_THEME, TASK_STATUS } from '@shared/constants'
export function showItemInFolder (fullPath, { errorMsg }) {
if (!fullPath) {
return
}
access(fullPath, constants.F_OK, (err) => {
console.log(`[Motrix] ${fullPath} ${err ? 'does not exist' : 'exists'}`)
if (err) {
Message.error(errorMsg)
return
}
shell.showItemInFolder(fullPath)
})
}
export const openItem = async (fullPath) => {
if (!fullPath) {
return
}
const result = await shell.openPath(fullPath)
return result
}
export function getTaskFullPath (task) {
const { dir, files, bittorrent } = task
let result = resolve(dir)
// Magnet link task
if (isMagnetTask(task)) {
return result
}
if (bittorrent && bittorrent.info && bittorrent.info.name) {
result = resolve(result, bittorrent.info.name)
return result
}
const [file] = files
const path = file.path ? resolve(file.path) : ''
let fileName = ''
if (path) {
result = path
} else {
if (files && files.length === 1) {
fileName = getFileNameFromFile(file)
if (fileName) {
result = resolve(result, fileName)
}
}
}
return result
}
export function moveTaskFilesToTrash (task) {
/**
* For magnet link tasks, there is bittorrent, but there is no bittorrent.info.
* The path is not a complete path before it becomes a BT task.
* In order to avoid accidentally deleting the directory
* where the task is located, it directly returns true when deleting.
*/
if (isMagnetTask(task)) {
return true
}
const { dir, status } = task
const path = getTaskFullPath(task)
if (!path || dir === path) {
throw new Error('task.file-path-error')
}
let deleteResult1 = true
access(path, constants.F_OK, (err) => {
console.log(`[Motrix] ${path} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
// Electron >= 12.x
// deleteResult1 = shell.trashItem(path)
deleteResult1 = shell.moveItemToTrash(path)
}
})
// There is no configuration file for the completed task.
if (status === TASK_STATUS.COMPLETE) {
return deleteResult1
}
let deleteResult2 = true
const extraFilePath = `${path}.aria2`
access(extraFilePath, constants.F_OK, (err) => {
console.log(`[Motrix] ${extraFilePath} ${err ? 'does not exist' : 'exists'}`)
if (!err) {
// Electron >= 12.x
// deleteResult2 = shell.trashItem(extraFilePath)
deleteResult2 = shell.moveItemToTrash(extraFilePath)
}
})
return deleteResult1 && deleteResult2
}
export function getSystemTheme () {
let result = APP_THEME.LIGHT
if (!is.macOS()) {
return result
}
result = nativeTheme.shouldUseDarkColors ? APP_THEME.DARK : APP_THEME.LIGHT
return result
}
export function isBigSur () {
return is.macOS() && getSystemMajorVersion() >= 20
}
export const delayDeleteTaskFiles = (task, delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const result = moveTaskFilesToTrash(task)
resolve(result)
} catch (err) {
reject(err.message)
}
}, delay)
})
}
+129
View File
@@ -0,0 +1,129 @@
import { isEmpty } from 'lodash'
import {
ADD_TASK_TYPE,
NONE_SELECTED_FILES,
SELECTED_ALL_FILES
} from '@shared/constants'
import { splitTaskLinks } from '@shared/utils'
import { buildOuts } from '@shared/utils/rename'
export const initTaskForm = state => {
const { addTaskUrl, addTaskOptions } = state.app
const {
allProxy,
dir,
engineMaxConnectionPerServer,
maxConnectionPerServer,
newTaskShowDownloading,
split
} = state.preference.config
const result = {
allProxy,
cookie: '',
dir,
engineMaxConnectionPerServer,
maxConnectionPerServer,
newTaskShowDownloading,
out: '',
referer: '',
selectFile: NONE_SELECTED_FILES,
split,
torrent: '',
uris: addTaskUrl,
userAgent: '',
...addTaskOptions
}
return result
}
export const buildHeader = (form) => {
const { userAgent, referer, cookie } = form
const result = []
if (!isEmpty(userAgent)) {
result.push(`User-Agent: ${userAgent}`)
}
if (!isEmpty(referer)) {
result.push(`Referer: ${referer}`)
}
if (!isEmpty(cookie)) {
result.push(`Cookie: ${cookie}`)
}
return result
}
export const buildOption = (type, form) => {
const {
allProxy,
dir,
out,
selectFile,
split
} = form
const result = {}
if (!isEmpty(allProxy)) {
result.allProxy = allProxy
}
if (!isEmpty(dir)) {
result.dir = dir
}
if (!isEmpty(out)) {
result.out = out
}
if (split > 0) {
result.split = split
}
if (type === ADD_TASK_TYPE.TORRENT) {
if (
selectFile !== SELECTED_ALL_FILES &&
selectFile !== NONE_SELECTED_FILES
) {
result.selectFile = selectFile
}
}
const header = buildHeader(form)
if (!isEmpty(header)) {
result.header = header
}
return result
}
export const buildUriPayload = (form) => {
let { uris, out } = form
if (isEmpty(uris)) {
throw new Error('task.new-task-uris-required')
}
uris = splitTaskLinks(uris)
const outs = buildOuts(uris, out)
const options = buildOption(ADD_TASK_TYPE.URI, form)
const result = {
uris,
outs,
options
}
return result
}
export const buildTorrentPayload = (form) => {
const { torrent } = form
if (isEmpty(torrent)) {
throw new Error('task.new-task-torrent-required')
}
const options = buildOption(ADD_TASK_TYPE.TORRENT, form)
const result = {
torrent,
options
}
return result
}
+68
View File
@@ -0,0 +1,68 @@
/* eslint no-unused-vars: 'off' */
import { TRAY_CANVAS_CONFIG } from '@shared/constants'
import { draw } from '@shared/utils/tray'
let idx = 0
let canvas
const initCanvas = () => {
if (canvas) {
return canvas
}
const { WIDTH, HEIGHT } = TRAY_CANVAS_CONFIG
return new OffscreenCanvas(WIDTH, HEIGHT)
}
const drawTray = async (payload) => {
self.postMessage({
type: 'log',
payload
})
if (!canvas) {
canvas = initCanvas()
}
try {
const tray = await draw({
canvas,
...payload
})
self.postMessage({
type: 'tray:drawed',
payload: {
idx,
tray
}
})
idx += 1
} catch (error) {
logger(error.message)
}
}
const logger = (text) => {
self.postMessage({
type: 'log',
payload: text
})
}
self.postMessage({
type: 'initialized',
payload: Date.now()
})
self.addEventListener('message', (event) => {
const { type, payload } = event.data
switch (type) {
case 'tray:draw':
drawTray(payload)
break
default:
logger(JSON.stringify(event.data))
}
})

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