178 Commits

Author SHA1 Message Date
09b2b4be10 4.1.2 2019-09-26 07:19:43 +02:00
f331de4134 Add Cmd+F shortcut to focus filter box 2019-09-26 07:08:26 +02:00
7760072e8e Avoid the filter box getting an outline in chrome 2019-09-26 07:02:12 +02:00
626bf592de List da locale in all.json 2019-09-26 06:38:49 +02:00
d550de0e27 Add Danish locale
Close #133
2019-09-26 06:37:49 +02:00
367efb4b53 Fix domain parsing in particular IPs
Closes #135
2019-09-26 06:34:23 +02:00
4a82c62958 Update zh_TW translation (#129) 2019-09-23 07:27:39 +02:00
891aa66bff Update zh_CN translation (#126) 2019-09-23 07:24:50 +02:00
2513b60c1b Update ko locale
Closes #127
2019-09-22 17:22:40 +02:00
31cca481f9 Update nl locale
Closes #128
2019-09-22 17:17:24 +02:00
31999bba9f Version 4.1.1 2019-09-21 09:48:56 +02:00
73d90662e3 Remove debug statement 2019-09-21 09:44:38 +02:00
5f5deb09f3 Add audios to DOM to avoid some playback errors 2019-09-21 09:41:40 +02:00
b68245f4f0 Do not treat CRASH as recoverable 2019-09-21 09:34:27 +02:00
34c8537cb5 Update locale list 2019-09-21 09:26:59 +02:00
6b7c9d461d Update ru translation 2019-09-21 09:26:01 +02:00
d2f09ca592 Added Bulgarian localization (#124) 2019-09-21 09:20:55 +02:00
919c6a8f10 more French strings (#120) 2019-09-19 01:52:30 +02:00
dcd7e7cd0e Update pl translation 2019-09-17 13:46:45 +02:00
c545bbab1d Update de locale 2019-09-17 13:46:45 +02:00
dc6e64e690 Add language specific donate URLs 2019-09-17 13:46:45 +02:00
578762db27 Update of lt locale (#117) 2019-09-17 13:39:41 +02:00
e996a2b41a Indonesia translation update (#115) 2019-09-17 03:43:56 +02:00
38496d9161 Hungarian translation update (#114) 2019-09-17 01:17:14 +02:00
4a0756aa26 Updated Spanish translation (#113) 2019-09-16 19:24:57 +02:00
63d0ff22fa Update Italian translation (#112)
Revised and updated translation, which contains work from me and @harbons
2019-09-16 18:31:17 +02:00
5390642978 Update Estonian translation (#111) 2019-09-16 18:20:11 +02:00
2e59dedda3 Japanese translation 4th (#110) 2019-09-16 18:19:33 +02:00
ee649717a2 Update Portuguese translation (#108) 2019-09-16 18:18:41 +02:00
078ce277ce Fix default subfolder suggestion.
Closes #109
2019-09-16 08:12:34 +02:00
7a3cad83b0 Japanese Translation 3rd (#107) 2019-09-15 19:12:53 +02:00
4d953c373f Basic import/export
Closes #64
2019-09-15 12:28:31 +02:00
da9832552f Update Greek translation
Closes #103
2019-09-15 09:51:14 +02:00
5909633a04 Updated Spanish translation (#101) 2019-09-15 09:45:19 +02:00
750fd987bd Update French translation
Closes #102
2019-09-15 09:43:01 +02:00
b87e0d6138 Japanese Translation 2nd (#100) 2019-09-14 21:33:40 +02:00
31cb23923a Add a subfolders dropdown
The dropdown will help users switch between folders easier than using the mask.
Also, the dropdown is more discoverable than masked folders.

See #2
2019-09-14 01:07:29 +02:00
71d98bc603 Use correct rows for tooltip ETA/status 2019-09-14 00:00:20 +02:00
4b09a0db67 Retries
Closes #95
2019-09-14 00:00:11 +02:00
da6c6bcf68 User Canceled error message 2019-09-13 23:09:44 +02:00
84fea3ba35 Allow to open Manager in a popup
Closes #47
2019-09-13 23:09:44 +02:00
33de1cbce9 Allow setting the referrer in select
Closes #78
2019-09-13 23:09:43 +02:00
04b8a981ef Allow to choose button action
Closes #51
2019-09-13 23:09:43 +02:00
58c7955c64 Add Delete Files action
Closes #92
2019-09-13 23:09:43 +02:00
dcf9603da8 Play done sound
Cloes #81
2019-09-13 23:09:43 +02:00
c2b9664b4b Version 4.0.12 2019-09-13 22:31:55 +02:00
e760d2b022 Handle Chrome state changes correct. 2019-09-12 20:54:16 +02:00
1a836d914b Preroll-nope 401, 402
Closes #89
2019-09-12 13:30:06 +02:00
1b0e6eb6c4 Send referrer with prerolls
See #89
2019-09-12 13:26:24 +02:00
39827ad485 Update TODO 2019-09-12 11:22:55 +02:00
79c4d4e98f typos in the de-locale 2019-09-12 10:00:36 +02:00
427bd2f348 Actually add Italian translation 2019-09-11 23:04:39 +02:00
4fefd0e128 Arabic translation
Closes #90
2019-09-11 23:03:25 +02:00
d79060237d Silence window-type errors 2019-09-10 14:43:49 +02:00
2df7a1c592 do not force ascii in addlocale 2019-09-10 14:43:48 +02:00
8c4ceb3e4b Privetize downloads 2019-09-10 12:44:56 +02:00
bf725ece72 Open links in the correct window context 2019-09-10 12:43:47 +02:00
76992bd4f4 Version 4.0.11 2019-09-10 09:37:00 +02:00
dccd530475 Typo 2019-09-10 09:33:32 +02:00
f1fa01a0eb Add ja to locale list 2019-09-10 09:30:59 +02:00
7949142ef6 Japanese translation (#83) 2019-09-10 09:29:12 +02:00
af1da8fc0a Fix maximized state being refused with dims
Closes #84
2019-09-10 09:23:43 +02:00
39f4237cde Update Indonesian translation (#80) 2019-09-09 18:07:07 +02:00
b676ed74cd Update locale list 2019-09-08 21:30:21 +02:00
bf474877ca Add Italian translation (#49) 2019-09-08 21:27:42 +02:00
7ee13af238 Version 4.0.10 2019-09-07 22:51:06 +02:00
d488e5874a Correct license header 2019-09-07 22:51:06 +02:00
b1a7c22452 Dismiss tooltip when the selection changes 2019-09-07 22:46:24 +02:00
e928d202ee Use the same base font size on all platforms 2019-09-07 20:13:37 +02:00
c39961d253 Make sure application/octet-stream is not mime-able 2019-09-07 19:39:43 +02:00
c6d11fcd7f Give MimeDB a class name 2019-09-07 19:34:12 +02:00
eb96103478 Sanitiy check detected names if we got a mime 2019-09-07 19:32:51 +02:00
583ccfc7b1 Detect name from query string 2019-09-07 19:27:28 +02:00
e0437718a0 Do not register remove-complete twice 2019-09-07 18:58:27 +02:00
2126ae022b Move preroller to it's own class 2019-09-07 18:27:31 +02:00
2ef39dcb19 Add some minor tests for mime 2019-09-07 17:54:12 +02:00
047c865e76 The things you see only after yu push... 2019-09-07 10:27:01 +02:00
c586cd00cc Switch to @Rob--W's CD parser
I "cleaned" up the library according to my personal preferences.
I tend to do this because in the process I need to develop a deeper
understanding of the code, and not because there was nothing wrong
with it.
I deeply appreciate the work that went into creating this library
and releasing it open source 👍

Closes #72
2019-09-07 10:22:09 +02:00
ee7f470269 Use ranges in preroll to avoid large downloads
This will effectively request the first 2 bytes from a server?
Why 2 bytes? Because a lot of servers are broken when bytes=0-0

Related to #70
2019-09-06 21:56:00 +02:00
f04dda308b Adding Hungarian locale, translation (#69) 2019-09-06 20:55:46 +02:00
071458e262 Switch preroll to GET
Fixes #70
2019-09-06 20:42:00 +02:00
9ffc96de4d Purge ourselves from history and sessions
Closes #66
2019-09-06 09:57:24 +02:00
AC
26e9a5404a Add Shift-Delete keyboard shortcut 2019-09-06 04:38:04 +02:00
f44fe59054 Polishing of some lt locale translations (#67) 2019-09-05 10:35:30 +02:00
e4b0629dee Version 4.0.9 2019-09-05 09:18:08 +02:00
5c2700ca36 Tooltip improvements 2019-09-05 09:03:50 +02:00
639a582804 Add small animation when opening files 2019-09-05 08:55:11 +02:00
2d1f185fcd Disable shelf in chrome
We cannot just disable it for our downloads (reliably)
so disable it completely while we're running.
2019-09-05 07:40:17 +02:00
38735ed0ae Make progress bar a little round 2019-09-05 07:39:19 +02:00
216bc590da Do not forget about 405 - Method not allowed 2019-09-04 21:34:52 +02:00
1c10d8005a Improve PREROLL based on user feedback 2019-09-04 21:27:03 +02:00
1fcfbe5360 Adjust default widths a bit 2019-09-04 21:15:32 +02:00
8d3dda1cec Bold buttons 2019-09-04 14:56:37 +02:00
be18f667d9 Trigger the saveQueue before reload 2019-09-04 14:48:35 +02:00
027b2c4fb1 Saving after preroll may have cause dupes 2019-09-04 14:48:35 +02:00
4ed92878be Update of zh_CN locale (#56) 2019-09-04 14:20:13 +02:00
a6930f309e Update ru locale 2019-09-04 14:15:06 +02:00
fdcdae0412 Use promises in content scripts 2019-09-04 14:15:06 +02:00
2c18ddaaa8 Increase shelf timeout 2019-09-04 14:15:06 +02:00
994e7ad0a6 Do not wait on downloads to finish prerollling 2019-09-04 14:15:06 +02:00
95536b36be Do not attempt to restart bg complete 2019-09-04 14:15:06 +02:00
9c159d5d24 Do not wait for the scheduler on startup 2019-09-04 14:15:06 +02:00
42ccfd5dc5 Do not abuse serverName to store browserName 2019-09-04 14:15:05 +02:00
dabf7f8a28 Prerolling and mime detection for some downloads
Only attempt this for a limited subset of downloads for now.
Related #45
2019-09-04 14:15:05 +02:00
9cac48f439 Make all tabs work again 2019-09-04 14:15:05 +02:00
ef6bc840d8 Do not trace 2019-09-04 14:15:00 +02:00
1c38ec1357 Add zh_TW locale (#62) 2019-09-04 12:03:50 +02:00
a4436bd6c8 Force Start should apply to Queued downloads
Closes #57
2019-09-03 18:18:07 +02:00
5a4b8143b2 Remove some any-types 2019-09-03 18:18:07 +02:00
00a5712427 Update of lt locale (#55) 2019-09-03 15:08:24 +02:00
6e55ee0745 Version 4.0.8 2019-09-03 13:32:09 +02:00
56098a382e Actually get most recent window 2019-09-03 13:23:00 +02:00
5ba9c7179b Korean locale
Closes #54
2019-09-03 13:07:10 +02:00
be9256ff1f Add Greek locale
Closes #53
2019-09-03 13:00:59 +02:00
92b2c32dd3 Add Russian locale
Closes #52
2019-09-03 12:57:48 +02:00
1935c7f444 Typos 2019-09-03 08:30:17 +02:00
4d48a2c395 Implement locale switcher 2019-09-03 08:24:29 +02:00
3cf30aaf08 Use _ version of language codes
Closes #50
2019-09-03 08:24:29 +02:00
262c3e169b Update TODO.md 2019-09-02 20:31:39 +02:00
2a0cb6720f Update Readme.md 2019-09-02 20:29:57 +02:00
295077dd75 Convert cs to new Chrome-compat
See: #46
2019-09-02 19:32:29 +02:00
c484f82cf5 Added czech translation 2019-09-02 19:29:27 +02:00
05aaac7ed8 Convert id translation to new Chrome-compatible format
See #45
2019-09-02 19:27:57 +02:00
9ef497028c Update text 2019-09-02 19:24:41 +02:00
380325bd43 Indonesian translation 2019-09-02 19:24:41 +02:00
164aa99eca Initial Chrome support
Part of #35
2019-09-02 18:05:06 +02:00
0a9155dcec Implement system icons
Closes #44
2019-09-02 14:52:14 +02:00
0bf9e76441 Fix incorrectly linked remove-missing-on-init 2019-09-02 14:45:31 +02:00
392681c1b7 Fix multi-filters
Closes #43
2019-09-02 13:33:38 +02:00
d4024a16ad Delay updates while queue is running
Closes #42
2019-09-02 13:16:39 +02:00
fba985482c Version 4.0.7 2019-09-02 12:05:13 +02:00
094fb0ee84 Fix additional placeholder in zh-CN 2019-09-02 12:04:39 +02:00
2ccc12de90 Version 4.0.6 2019-09-02 11:53:03 +02:00
28e6866db8 Use currentWindow instead of WINDOW_ID_CURRENT 2019-09-02 04:06:28 +02:00
7185964649 emulate must use correct window
Closes #41
2019-09-02 04:02:50 +02:00
cd1005823d Typo fixes 2019-09-02 03:57:21 +02:00
0afceb9850 Dutch translation
Closes #39
2019-09-02 00:30:45 +02:00
e6dc205b9d Small de improvement 2019-09-02 00:26:35 +02:00
8e473c778b Rename to zh-CN
Related to #38
2019-09-02 00:26:15 +02:00
7a71ae5f37 Also split ui locales by _ 2019-09-02 00:24:11 +02:00
d04f3db22f Add Simplified Chinese translation
Created this translation by using the online translation tool from https://downthemall.github.io/translate/ .
2019-09-02 00:23:51 +02:00
adc6b9dbb2 Add et locale 2019-09-02 00:03:29 +02:00
e8f09c80f3 Do not add empty separator in URLMenuFilter 2019-09-01 15:24:14 +02:00
46c4e66558 webpack config updates 2019-09-01 06:17:05 +02:00
a8ea416a67 Remove smartphone section from issue template 2019-09-01 05:06:30 +02:00
320c1ddafa Add a little margin to allow focus outlines 2019-09-01 05:01:49 +02:00
4c576ba720 Add content_security_policy 2019-09-01 04:52:57 +02:00
7b58779f9e Remove outdated todos 2019-09-01 00:37:10 +02:00
45d835fe19 use correct autoHide HTML spelling 2019-09-01 00:03:53 +02:00
2d2826d192 Correctly toggle URLMenuFilter 2019-09-01 00:03:34 +02:00
4c77ad0f1f Fix popup height flash bug 2019-08-31 23:16:46 +02:00
4d72ac4534 Notifications galore 2019-08-31 23:08:12 +02:00
cdda0835d8 Wrong translation 2019-08-31 18:12:37 +02:00
78e91304eb Version 4.0.5 2019-08-31 15:44:35 +02:00
0702631003 nag a little later 2019-08-31 13:40:54 +02:00
c45bf671fb fixes to polish translation (#34) 2019-08-31 13:38:47 +02:00
33a3e275fc Create messages.json 2019-08-31 08:16:29 +02:00
369514f155 Add lt locale 2019-08-30 17:02:08 +02:00
494479ce1a Version 4.0.4 2019-08-29 23:09:14 +02:00
98ebb160f9 Silence a console.log 2019-08-29 19:37:09 +02:00
d1cc406f05 select: frontend/backend mappings were out of sync
Closes #27
2019-08-29 19:36:06 +02:00
687b6e1aa9 pl locale
Closes #19
2019-08-29 09:52:26 +02:00
c960d48b72 es-ES locale
Closes #26
2019-08-29 08:24:11 +02:00
c8c7506efc Properly translate Filters heading
Reported in #26
2019-08-29 08:14:22 +02:00
91edcee28c FIx filters message names 2019-08-29 08:14:22 +02:00
a425a786ef Do not log "custom" 2019-08-29 08:14:22 +02:00
f023351acc Update Readme.md 2019-08-28 18:39:44 +02:00
c8610eee29 Add some additional file exts to the def filters 2019-08-28 17:59:29 +02:00
f7a70ec2ea Add fr-FR locale 2019-08-28 17:16:34 +02:00
69d8ffe8a5 Minor typos 2019-08-28 05:52:32 +02:00
71240ec1e8 More typos 2019-08-28 05:45:06 +02:00
a6f3c7a647 Some typos 2019-08-28 05:41:41 +02:00
eab9631a11 Fix typo 2019-08-28 04:10:24 +02:00
9e29db911c Update dependencies 2019-08-27 19:37:55 +02:00
8d1040115a Update information about translations 2019-08-27 19:32:11 +02:00
3551545eae add DE locale 2019-08-27 19:12:36 +02:00
01001dc7b2 Add missing i18n and fix some typos 2019-08-27 18:53:31 +02:00
b4e6ab80d2 Add locale descriptions 2019-08-27 16:43:49 +02:00
108 changed files with 31605 additions and 1232 deletions

View File

@ -7,6 +7,11 @@ assignees: ''
---
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Describe the bug**
A clear and concise description of what the bug is.
@ -23,16 +28,6 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -30,7 +30,7 @@ THE SOFTWARE.
## DownThemAll! uikit
Copyright © 2016-2019 by Nils Maier
The uikit libraries and assets are licened under the MIT license.
The uikit libraries and assets are licensed under the MIT license.
## DownThemAll! interface (.html, .css)
@ -41,7 +41,8 @@ Licensed under GPL2.0; see [LICENSE.gpl-2.0.txt](LICENSE.gpl-2.0.txt).
## DownThemAll! icons, icon-font and graphic assets
Copyright (C) 2012-2019 by Nils Maier
Licensed under Creative Commons Attribution-ShareAlike 4.0 International
Licensed under Creative Commons Attribution-ShareAlike 4.0 International.
The icon font contains icons from Font Awesome.
See: https://creativecommons.org/licenses/by-sa/4.0/legalcode
@ -54,21 +55,35 @@ Copyright © 2010-2019 by Nils Maier, Stefano Verna.
The DownThemAll! name and logo cannot be used without explicit permission
in any derivative work, except in credits and license-related notices.
Using the DownThemAll! logo in personal non-distributed non-commerical
Using the DownThemAll! logo in personal non-distributed non-commercial
modifications of the software and forks is permitted without explicit
permission.
Distributing official DownThemAll! releases without any modifications is allowed without explicit permission.
## Font Awesome
Copyright (C) 2016 by Dave Gandy
License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/
## webextension-polyfill
Lcensed under the Mozilla Public License 2.0.
Licensed under the Mozilla Public License 2.0.
## PSL (public-suffix-list)
The list itself is licensed under the Mozilla Public License 2.0.
The javascript library accessing it is licensed under the MIT license.
The javascript library accessing it is licensed under the MIT license.
## whatwg-mimetype
Licensed under MIT
## CDHeaderParser
Licensed under MPL-2
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)

View File

@ -1,12 +1,10 @@
DownThemAll! WE
===
# DownThemAll! WE
The DownThemAll! WebExtension.
For those still on supported browser: [Non-WebExtension legacy code](https://github.com/downthemall/downthemall-legacy).
About
---
## About
This is the WebExtension version of DownThemAll!, a complete re-development from scratch.
Being a WebExtension it lacks a ton of features the original DownThemAll! had. Sorry, but there is no way around it since Mozilla decided to adopt WebExtensions as the *only* extension type and WebExtensions are extremely limited in what they can do.
@ -23,23 +21,48 @@ But it is what it is...
**What we *can* do and did do is bring the mass selection, organizing (renaming masks, etc) and queueing tools of DownThemAll! over to the WebExtension, so you can easily queue up hundreds or thousands files at once without the downloads going up in flames because the browser tried to download them all at once.**
Translations
---
## Translations
If you would like to help out translating DTA, please see our [translation guide](_locales/Readme.md).
Development
---
## Development
You will want to `yarn` the development dependencies such as webpack first.
Afterwards there is two important commands to run
Afterwards, you will want to run`yarn watch`.
This will run the webpack bundler in watch mode, transpiling the TypeScript to Javascript and updating bundles as you change the source.
* `yarn watch` - This will run the webpack bundler in watch mode, updating bundles as you change the source.
* `yarn webext` - This will run the WebExtension in a development profile using the [`web-ext` tool from mozilla](https://www.npmjs.com/package/web-ext) (which you need to install separately).
Please note: You have to run `yarn watch` (at least once) as it builds the actual script bundles.
### Firefox
I recommend you install the [`web-ext`](https://www.npmjs.com/package/web-ext) tools from mozilla. It is not listed as a dependency by design at it causes problems with dependency resolution in yarn right now if installed in the same location as the rest of the dependencies.
If you did, then running `yarn webext` (additionally to `yarn watch`) will run the WebExtension in a development profile. This will use the directory `../dtalite.p` to keep a development profile. You might need to create this directory before you use this command. Furthermore `yarn webext` will watch for changes to the sources and automatically reload the extension.
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox).
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox with the right about:config preferences).
Before submitting patches, please make sure you run eslint, if this isn't done automatically, and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum.
### Chrome
The code base is comparatively large for a WebExtension, with over 10K sloc of typescript and over 14K sloc total.
You have to build the bundles first, of course.
Then put your Chrome into Developement Mode on the Extensions page, and Load Unpacked the directory of your downthemall clone.
### Patches
Before submitting patches, please make sure you run eslint (if this isn't done automatically in your text editor/IDE), and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum and need justification.
Please submit your patches as Pull Requests, and rebase your commits onto the current `master` before submitting.
### Code structure
The code base is comparatively large for a WebExtension, with over 11K sloc of typescript.
It isn't as well organized as it should be in some places; hope you don't mind.
* `uikit/` - The base User Interface Kit, which currently consists of
* the `VirtualTable` implementation, aka that interactive HTML table with columns, columns resizing and hiding, etc you see in the Manager, Select and Preferences windows/tabs
* the `ContextMenu` and related classes that drive the HTML-based context menus
* `lib/` - The "backend stuff" and assorted library routines and classes.
* `windows/` - The "frontend stuff" so all the HTML and corresponding code to make that HTML into something interactive
* `style/` - CSS and images

11
TODO.md
View File

@ -8,17 +8,9 @@ P2
Planned for later.
* Soft errors and retry logic
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
* Delete files (well, as far as the browser allows)
* Inter-addon API (basic)
* Add downloads
* Chrome support
* vtable perf: cache column widths
* Localizations
* Settle on system
* Do the de-locale
* Enagage translators
* Download options
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
@ -39,7 +31,6 @@ Nice-to-haves.
* Dark Theme support
* os/browser define be default
* overwritable
* Get and cache system icons (because Firefox doesn't allow moz-icon: for WE, but makes them kinda accessible through the downloads API anyway, essentially copying them via a canvas on a privileged hidden page into a data URL... ikr)
* Remove `any` types as possible, and generally improve typescript (new language to me)
P4
@ -51,8 +42,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
* Segmented downloads
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
* Handle errors, 404 and such
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
* Conflicts: ask when a file exists
* Not supported by Firefox
* Speed limiter

View File

@ -1,20 +1,33 @@
# Translations
Right now we did not standardize on a tool to translate, so feel free to whip our your favorite text edits, JSON editor, special translation tool, what have you.
Right now we did not standardize on a tool/website/community use for translations
To make a translation of DownThemAll! in your language, please:
## Website-based Translation
Please go to [https://downthemall.github.io/translate/](https://downthemall.github.io/translate/) for a "good enough" tool to translate DownThemAll! for now. It will load the English locale as a base automatically.
Then you can translate (your progress will be saved in the browser). Once done, you can Download the `messages.json` and test it or submit it for inclusion.
You can also import your or other people's existing translations to modify. This will overwrite any progress you made so far, tho.
## Manual Translation
* Get the [`en/messages.json`](https://github.com/downthemall/downthemall/raw/master/_locales/en/messages.json) as a base.
* Translate the `"message"` items in that file only.
* Do not translate anything other.
* Translate the `"message"` items in that file only. Whip our your favorite text editor, JSON editor, special translation tool, what have you.
* Do not translate anything besides the "message" elements. Pay attention to the descriptions.
* Do not remove anything.
* Do not translate `$PLACEHOLDERS$`. Placeholders should appear in your translation with the same spelling and all uppercase.
They will be relaced at runtime with actual values.
* Make sure you save the file in an "utf-8" encoding. If you need double quotes, you need to escape the quotes with a backslash, e.g. `"some \"quoted\" text"`
* You should translate all strings. If you want to skip a string, set it to an empty `""` string. DTA will then use the English string.
* Once you are at a point you want to test things:
* Go to the DownThemAll! Preferences where you will find a "Load custom translation" button.
* Select your translated `messages.json`. (it doesn't have to be named exactly like that, but should have a `.json` extension)
* Make sure you save the file in an "utf-8" encoding. If you need double quotes, you need to escape the quotes with a backslash, e.g. `"some \"quoted\" text"`
* You should translate all strings. If you want to skip a string, set it to an empty `""` string. DTA will then use the English string.
## Testing Your Translation
* Go to the DownThemAll! Preferences where you will find a "Load custom translation" button.
* Select your translated `messages.json`. (it doesn't have to be named exactly like that, but should have a `.json` extension)
* If everything was OK, you will be asked to reload the extension (this will only reload DTA not the entire browser).
* See your strings in action once you reloaded DTA (either by answering OK when asked, or disable/enable the extension manually or restart your browser).
* If you're happy with the result and would like to contribute it back, you can either file a full Pull Request, or just file an issue and post a link to e.g. a [gist](https://gist.github.com/) or paste the translation in the issue text.
* See your strings in action once you reloaded DTA (either by answering OK when asked, or disable/enable the extension manually or restart your browser).
## Submitting Your Translation
If you're happy with the result and would like to contribute it back, you can either file a full Pull Request, or just file an issue and post a link to e.g. a [gist](https://gist.github.com/) or paste the translation in the issue text.

24
_locales/all.json Normal file
View File

@ -0,0 +1,24 @@
{
"ar": "العربية [ar]",
"bg": "Български [bg]",
"cs": "Čeština (CZ) [cs]",
"da": "Dansk [da]",
"de": "Deutsch [de]",
"el": "Ελληνικά [el]",
"en": "English (US) [en]",
"es": "Español (España) [es]",
"et": "Eesti keel [et]",
"fr": "Français [fr]",
"hu": "Magyar (HU) [hu]",
"id": "Bahasa Indonesia [id]",
"it": "Italiano [it]",
"ja": "日本語 (JP) [ja]",
"ko": "한국어 [ko]",
"lt": "Lietuvių [lt]",
"nl": "Nederlands [nl]",
"pl": "Polski [pl]",
"pt": "Português (Brasil) [pt]",
"ru": "Русский [ru]",
"zh_CN": "简体中文 [zh_CN]",
"zh_TW": "正體中文 [zh_TW]"
}

1170
_locales/ar/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/bg/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/cs/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/da/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/de/messages.json Normal file

File diff suppressed because it is too large Load Diff

1264
_locales/el/messages.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1284
_locales/es/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/et/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/fr/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/hu/messages.json Normal file

File diff suppressed because it is too large Load Diff

1136
_locales/id/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/it/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/ja/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/ko/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/lt/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/nl/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/pl/messages.json Executable file

File diff suppressed because it is too large Load Diff

1284
_locales/pt/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/ru/messages.json Executable file

File diff suppressed because it is too large Load Diff

1284
_locales/zh_CN/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/zh_TW/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
},
"deffilter-aud": {
"label": "Audio",
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape)$/i",
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
"type": 1,
"active": false,
"icon": "mp3"
@ -35,7 +35,7 @@
},
"deffilter-img": {
"label": "Images",
"expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico)$/i",
"expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico|heic|heif|webp|jxr|wdp|dng|cr2|arw)$/i",
"type": 3,
"active": false,
"icon": "jpg"
@ -63,7 +63,7 @@
},
"deffilter-vid": {
"label": "Videos",
"expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv)$/i",
"expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv|f4v|m4v)$/i",
"type": 3,
"active": true,
"icon": "mkv"

View File

@ -80,7 +80,11 @@
"tif",
"tiff",
"wmf",
"webp"
"webp",
"heic",
"heif",
"jxr",
"wdp"
],
"video": [
"3g2",

396
data/mime.json Normal file
View File

@ -0,0 +1,396 @@
{
"e": {
"3gpp": "3gp",
"asx": "asf",
"gz": "gzip",
"heic": "heif",
"html": [
"htm",
"shtml",
"php"
],
"jar": [
"war",
"ear"
],
"jpg": [
"jpeg",
"jpe",
"jfif"
],
"js": "jsx",
"mid": [
"midi",
"kar"
],
"mkv": [
"mk3d",
"mks"
],
"mov": [
"qt",
"moov"
],
"mpg": [
"mpe",
"mpeg"
],
"pem": [
"crt",
"der"
],
"pl": "pm",
"prc": "pdb",
"ps": [
"eps",
"ai"
],
"svg": "svgz",
"tcl": "tk",
"tif": "tiff"
},
"m": {
"application/7z": "7z",
"application/7z-compressed": "7z",
"application/ai": "ps",
"application/atom": "atom",
"application/atom+xml": "atom",
"application/bz2": "bz2",
"application/bzip2": "bz2",
"application/cco": "cco",
"application/cocoa": "cco",
"application/compressed": "gz",
"application/crt": "pem",
"application/der": "pem",
"application/doc": "doc",
"application/ear": "jar",
"application/eot": "eot",
"application/eps": "ps",
"application/gz": "gz",
"application/gzip": "gz",
"application/hqx": "hqx",
"application/jar": "jar",
"application/jardiff": "jardiff",
"application/java-archive": "jar",
"application/java-archive-diff": "jardiff",
"application/java-jnlp-file": "jnlp",
"application/javascript": "js",
"application/jnlp": "jnlp",
"application/js": "js",
"application/json": "json",
"application/jsx": "js",
"application/kml": "kml",
"application/kmz": "kmz",
"application/m3u8": "m3u8",
"application/mac-binhex40": "hqx",
"application/makeself": "run",
"application/msword": "doc",
"application/odg": "odg",
"application/odp": "odp",
"application/ods": "ods",
"application/odt": "odt",
"application/pdb": "prc",
"application/pdf": "pdf",
"application/pem": "pem",
"application/perl": "pl",
"application/pilot": "prc",
"application/pl": "pl",
"application/pm": "pl",
"application/postscript": "ps",
"application/ppt": "ppt",
"application/prc": "prc",
"application/ps": "ps",
"application/rar": "rar",
"application/rar-compressed": "rar",
"application/redhat-package-manager": "rpm",
"application/rpm": "rpm",
"application/rss": "rss",
"application/rss+xml": "rss",
"application/rtf": "rtf",
"application/run": "run",
"application/sea": "sea",
"application/shockwave-flash": "swf",
"application/sit": "sit",
"application/stuffit": "sit",
"application/swf": "swf",
"application/tar": "tar",
"application/tcl": "tcl",
"application/tk": "tcl",
"application/vnd.apple.mpegurl": "m3u8",
"application/vnd.google-earth.kml+xml": "kml",
"application/vnd.google-earth.kmz": "kmz",
"application/vnd.ms-excel": "xls",
"application/vnd.ms-fontobject": "eot",
"application/vnd.ms-powerpoint": "ppt",
"application/vnd.oasis.opendocument.graphics": "odg",
"application/vnd.oasis.opendocument.presentation": "odp",
"application/vnd.oasis.opendocument.spreadsheet": "ods",
"application/vnd.oasis.opendocument.text": "odt",
"application/vnd.wap.wmlc": "wmlc",
"application/war": "jar",
"application/wmlc": "wmlc",
"application/x-7z": "7z",
"application/x-7z-compressed": "7z",
"application/x-ai": "ps",
"application/x-atom": "atom",
"application/x-atom+xml": "atom",
"application/x-bz2": "bz2",
"application/x-bzip2": "bz2",
"application/x-cco": "cco",
"application/x-cocoa": "cco",
"application/x-compressed": "gz",
"application/x-crt": "pem",
"application/x-der": "pem",
"application/x-doc": "doc",
"application/x-ear": "jar",
"application/x-eot": "eot",
"application/x-eps": "ps",
"application/x-gz": "gz",
"application/x-gzip": "gz",
"application/x-hqx": "hqx",
"application/x-jar": "jar",
"application/x-jardiff": "jardiff",
"application/x-java-archive": "jar",
"application/x-java-archive-diff": "jardiff",
"application/x-java-jnlp-file": "jnlp",
"application/x-javascript": "js",
"application/x-jnlp": "jnlp",
"application/x-js": "js",
"application/x-json": "json",
"application/x-jsx": "js",
"application/x-kml": "kml",
"application/x-kmz": "kmz",
"application/x-m3u8": "m3u8",
"application/x-mac-binhex40": "hqx",
"application/x-makeself": "run",
"application/x-msword": "doc",
"application/x-odg": "odg",
"application/x-odp": "odp",
"application/x-ods": "ods",
"application/x-odt": "odt",
"application/x-pdb": "prc",
"application/x-pdf": "pdf",
"application/x-pem": "pem",
"application/x-perl": "pl",
"application/x-pilot": "prc",
"application/x-pl": "pl",
"application/x-pm": "pl",
"application/x-postscript": "ps",
"application/x-ppt": "ppt",
"application/x-prc": "prc",
"application/x-ps": "ps",
"application/x-rar": "rar",
"application/x-rar-compressed": "rar",
"application/x-redhat-package-manager": "rpm",
"application/x-rpm": "rpm",
"application/x-rss": "rss",
"application/x-rss+xml": "rss",
"application/x-rtf": "rtf",
"application/x-run": "run",
"application/x-sea": "sea",
"application/x-shockwave-flash": "swf",
"application/x-sit": "sit",
"application/x-stuffit": "sit",
"application/x-swf": "swf",
"application/x-tar": "tar",
"application/x-tcl": "tcl",
"application/x-tk": "tcl",
"application/x-vnd.apple.mpegurl": "m3u8",
"application/x-vnd.google-earth.kml+xml": "kml",
"application/x-vnd.google-earth.kmz": "kmz",
"application/x-vnd.ms-excel": "xls",
"application/x-vnd.ms-fontobject": "eot",
"application/x-vnd.ms-powerpoint": "ppt",
"application/x-vnd.oasis.opendocument.graphics": "odg",
"application/x-vnd.oasis.opendocument.presentation": "odp",
"application/x-vnd.oasis.opendocument.spreadsheet": "ods",
"application/x-vnd.oasis.opendocument.text": "odt",
"application/x-vnd.wap.wmlc": "wmlc",
"application/x-war": "jar",
"application/x-wmlc": "wmlc",
"application/x-x509-ca-cert": "pem",
"application/x-xhtml": "xhtml",
"application/x-xhtml+xml": "xhtml",
"application/x-xls": "xls",
"application/x-xpi": "xpi",
"application/x-xpinstall": "xpi",
"application/x-xspf": "xspf",
"application/x-xspf+xml": "xspf",
"application/x-xz": "xz",
"application/x-zip": "zip",
"application/x509-ca-cert": "pem",
"application/xhtml": "xhtml",
"application/xhtml+xml": "xhtml",
"application/xls": "xls",
"application/xpi": "xpi",
"application/xpinstall": "xpi",
"application/xspf": "xspf",
"application/xspf+xml": "xspf",
"application/xz": "xz",
"application/zip": "zip",
"audio/kar": "mid",
"audio/m4a": "m4a",
"audio/matroska": "mka",
"audio/mid": "mid",
"audio/midi": "mid",
"audio/mka": "mka",
"audio/mp3": "mp3",
"audio/mpeg": "mp3",
"audio/ogg": "ogg",
"audio/ra": "ra",
"audio/realaudio": "ra",
"audio/x-kar": "mid",
"audio/x-m4a": "m4a",
"audio/x-matroska": "mka",
"audio/x-mid": "mid",
"audio/x-midi": "mid",
"audio/x-mka": "mka",
"audio/x-mp3": "mp3",
"audio/x-mpeg": "mp3",
"audio/x-ogg": "ogg",
"audio/x-ra": "ra",
"audio/x-realaudio": "ra",
"font/woff": "woff",
"font/woff2": "woff2",
"font/x-woff": "woff",
"font/x-woff2": "woff2",
"image/bmp": "bmp",
"image/gif": "gif",
"image/heic": "heic",
"image/heif": "heic",
"image/heif-sequence": "heic",
"image/ico": "ico",
"image/icon": "ico",
"image/jfif": "jpg",
"image/jng": "jng",
"image/jpe": "jpg",
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/ms-bmp": "bmp",
"image/png": "png",
"image/svg": "svg",
"image/svg+xml": "svg",
"image/svgz": "svg",
"image/tif": "tif",
"image/tiff": "tif",
"image/vnd.wap.wbmp": "wbmp",
"image/wbmp": "wbmp",
"image/webp": "webp",
"image/x-bmp": "bmp",
"image/x-gif": "gif",
"image/x-heic": "heic",
"image/x-heif": "heic",
"image/x-heif-sequence": "heic",
"image/x-ico": "ico",
"image/x-icon": "ico",
"image/x-jfif": "jpg",
"image/x-jng": "jng",
"image/x-jpe": "jpg",
"image/x-jpeg": "jpg",
"image/x-jpg": "jpg",
"image/x-ms-bmp": "bmp",
"image/x-png": "png",
"image/x-svg": "svg",
"image/x-svg+xml": "svg",
"image/x-svgz": "svg",
"image/x-tif": "tif",
"image/x-tiff": "tif",
"image/x-vnd.wap.wbmp": "wbmp",
"image/x-wbmp": "wbmp",
"image/x-webp": "webp",
"text/component": "htc",
"text/css": "css",
"text/htc": "htc",
"text/htm": "html",
"text/html": "html",
"text/jad": "jad",
"text/javascript": "js",
"text/js": "js",
"text/jsx": "js",
"text/mathml": "mml",
"text/mml": "mml",
"text/php": "html",
"text/plain": "txt",
"text/shtml": "html",
"text/txt": "txt",
"text/vnd.sun.j2me.app-descriptor": "jad",
"text/vnd.wap.wml": "wml",
"text/wml": "wml",
"text/x-component": "htc",
"text/x-css": "css",
"text/x-htc": "htc",
"text/x-htm": "html",
"text/x-html": "html",
"text/x-jad": "jad",
"text/x-javascript": "js",
"text/x-js": "js",
"text/x-jsx": "js",
"text/x-mathml": "mml",
"text/x-mml": "mml",
"text/x-php": "html",
"text/x-plain": "txt",
"text/x-shtml": "html",
"text/x-txt": "txt",
"text/x-vnd.sun.j2me.app-descriptor": "jad",
"text/x-vnd.wap.wml": "wml",
"text/x-wml": "wml",
"text/x-xml": "xml",
"text/xml": "xml",
"video/3gp": "3gpp",
"video/3gpp": "3gpp",
"video/asf": "asx",
"video/asx": "asx",
"video/avi": "avi",
"video/flv": "flv",
"video/m4v": "m4v",
"video/matroska": "mkv",
"video/mk3d": "mkv",
"video/mks": "mkv",
"video/mkv": "mkv",
"video/mng": "mng",
"video/moov": "mov",
"video/mov": "mov",
"video/mp2t": "ts",
"video/mp4": "mp4",
"video/mpe": "mpg",
"video/mpeg": "mpg",
"video/mpg": "mpg",
"video/ms-asf": "asx",
"video/ms-wmv": "wmv",
"video/msvideo": "avi",
"video/opus": "opus",
"video/qt": "mov",
"video/quicktime": "mov",
"video/ts": "ts",
"video/webm": "webm",
"video/wmv": "wmv",
"video/x-3gp": "3gpp",
"video/x-3gpp": "3gpp",
"video/x-asf": "asx",
"video/x-asx": "asx",
"video/x-avi": "avi",
"video/x-flv": "flv",
"video/x-m4v": "m4v",
"video/x-matroska": "mkv",
"video/x-mk3d": "mkv",
"video/x-mks": "mkv",
"video/x-mkv": "mkv",
"video/x-mng": "mng",
"video/x-moov": "mov",
"video/x-mov": "mov",
"video/x-mp2t": "ts",
"video/x-mp4": "mp4",
"video/x-mpe": "mpg",
"video/x-mpeg": "mpg",
"video/x-mpg": "mpg",
"video/x-ms-asf": "asx",
"video/x-ms-wmv": "wmv",
"video/x-msvideo": "avi",
"video/x-opus": "opus",
"video/x-qt": "mov",
"video/x-quicktime": "mov",
"video/x-ts": "ts",
"video/x-webm": "webm",
"video/x-wmv": "wmv"
}
}

View File

@ -1,18 +1,22 @@
{
"global-turbo": false,
"button-type": "popup",
"manager-in-popup": false,
"concurrent": 4,
"queue-notification": true,
"finish-notification": true,
"sounds": true,
"open-manager-on-queue": true,
"text-links": true,
"add-paused": false,
"hide-context": false,
"conflict-action": "uniquify",
"nagging": 0,
"nagging-next": 6,
"nagging-next": 7,
"tooltip": true,
"show-urls": false,
"remove-missing-on-init": false,
"retries": 5,
"retry-time": 10,
"limits": [
{
"domain": "*",

View File

@ -11,7 +11,7 @@ import { getManager } from "./manager/man";
import { select } from "./select";
import { single } from "./single";
import { Notification } from "./notifications";
import { MASK, FASTFILTER } from "./recentlist";
import { MASK, FASTFILTER, SUBFOLDER } from "./recentlist";
import { openManager } from "./windowutils";
import { _ } from "./i18n";
@ -19,6 +19,7 @@ const MAX_BATCH = 10000;
export interface QueueOptions {
mask?: string;
subfolder?: string;
paused?: boolean;
}
@ -28,8 +29,9 @@ export const API = new class APIImpl {
}
async queue(items: BaseItem[], options: QueueOptions) {
await MASK.init();
await Promise.all([MASK.init(), SUBFOLDER.init()]);
const {mask = MASK.current} = options;
const {subfolder = SUBFOLDER.current} = options;
const {paused = false} = options;
const defaults: any = {
@ -46,6 +48,7 @@ export const API = new class APIImpl {
private: false,
postData: null,
mask,
subfolder,
date: Date.now(),
paused
};
@ -117,6 +120,10 @@ export const API = new class APIImpl {
await FASTFILTER.init();
await FASTFILTER.push(options.fast);
}
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
await SUBFOLDER.init();
await SUBFOLDER.push(options.subfolder);
}
if (typeof options.type === "string") {
await Prefs.set("last-type", options.type);
}
@ -128,7 +135,6 @@ export const API = new class APIImpl {
return false;
}
const {items, options} = await select(links, media);
console.log(items, options);
return this.regularInternal(items, options);
}

View File

@ -17,7 +17,11 @@ import {
// eslint-disable-next-line no-unused-vars
Tab,
// eslint-disable-next-line no-unused-vars
MenuClickInfo
MenuClickInfo,
CHROME,
runtime,
history,
sessions,
} from "./browser";
import { Bus } from "./bus";
import { filterInSitu } from "./util";
@ -27,9 +31,26 @@ const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
const GATHER = "/bundles/content-gather.js";
const CHROME_CONTEXTS = Object.freeze(new Set([
"all",
"audio",
"browser_action",
"editable",
"frame",
"image",
"launcher",
"link",
"page",
"page_action",
"selection",
"video",
]));
async function runContentJob(tab: Tab, file: string, msg: any) {
try {
if (tab && tab.incognito && msg) {
msg.private = tab.incognito;
}
const res = await tabs.executeScript(tab.id, {
file,
allFrames: true,
@ -83,11 +104,15 @@ class Handler {
async performSelection(options: SelectionOptions) {
try {
const toptions: any = {
currentWindow: true,
discarded: false,
};
if (!CHROME) {
toptions.hidden = false;
}
const selectedTabs = options.allTabs ?
await tabs.query({
currentWindow: true,
discarded: false,
hidden: false}) as any[] :
await tabs.query(toptions) as any[] :
[options.tab];
const textLinks = await Prefs.get("text-links", true);
@ -111,45 +136,24 @@ class Handler {
}
locale.then(() => {
new class Action extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
}
async onClicked(tab: {id: number}) {
if (!tab.id) {
return;
}
try {
await this.processResults(
true,
await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
}
catch (ex) {
console.error(ex);
}
}
}();
const menuHandler = new class Menus extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
const alls = new Map<string, string[]>();
const mcreate = (options: any) => {
if (CHROME) {
delete options.icons;
options.contexts = options.contexts.
filter((e: string) => CHROME_CONTEXTS.has(e));
if (!options.contexts.length) {
return;
}
}
if (options.contexts.includes("all")) {
alls.set(options.id, options.contexts);
}
return menus.create(options);
menus.create(options);
};
mcreate({
id: "DTARegularLink",
@ -388,8 +392,11 @@ locale.then(() => {
}
}
async enumulate(action: string) {
const tab = await tabs.query({active: true});
async emulate(action: string) {
const tab = await tabs.query({
active: true,
currentWindow: true,
});
if (!tab || !tab.length) {
return;
}
@ -507,32 +514,127 @@ locale.then(() => {
}
}();
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
new class Action extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
Prefs.get("button-type", false).then(v => this.adjust(v));
Prefs.on("button-type", (prefs, key, value) => {
this.adjust(value);
});
}
adjust(type: string) {
action.setPopup({
popup: type !== "popup" ? "" : "/windows/popup.html"
});
let icons;
switch (type) {
case "popup":
icons = {
16: "/style/icon16.png",
32: "/style/icon32.png",
48: "/style/icon48.png",
64: "/style/icon64.png",
96: "/style/icon96.png",
128: "/style/icon128.png",
256: "/style/icon256.png"
};
break;
case "dta":
icons = {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
};
break;
case "turbo":
icons = {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
};
break;
case "manager":
icons = {
16: "/style/button-manager.png",
32: "/style/button-manager@2x.png",
};
break;
}
action.setIcon({path: icons});
}
async onClicked() {
switch (await Prefs.get("button-type")) {
case "popup":
break;
case "dta":
menuHandler.emulate("DTARegular");
break;
case "turbo":
menuHandler.emulate("DTATurbo");
break;
case "manager":
menuHandler.emulate("DTAManager");
break;
}
}
}();
Bus.on("do-regular", () => menuHandler.emulate("DTARegular"));
Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll"));
Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo"));
Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll"));
Bus.on("do-single", () => API.singleRegular(null));
Bus.on("open-manager", () => openManager(true));
Bus.on("open-prefs", () => openPrefs());
function adjustAction(globalTurbo: boolean) {
action.setPopup({
popup: globalTurbo ? "" : null
});
action.setIcon({
path: globalTurbo ? {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
} : null
});
}
(async function init() {
await Prefs.set("last-run", new Date());
Prefs.get("global-turbo", false).then(v => adjustAction(v));
Prefs.on("global-turbo", (prefs, key, value) => {
adjustAction(value);
const urlBase = runtime.getURL("");
history.onVisited.addListener(({url}: {url: string}) => {
if (!url || !url.startsWith(urlBase)) {
return;
}
history.deleteUrl({url});
});
const results: {url?: string}[] = await history.search({text: urlBase});
for (const {url} of results) {
if (!url) {
continue;
}
history.deleteUrl({url});
}
if (!CHROME) {
const sessionRemover = async () => {
for (const s of await sessions.getRecentlyClosed()) {
if (s.tab) {
if (s.tab.url.startsWith(urlBase)) {
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
}
continue;
}
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
continue;
}
const [tab] = s.window.tabs;
if (tab.url.startsWith(urlBase)) {
await sessions.forgetClosedWindow(s.window.sessionId);
}
}
};
sessions.onChanged.addListener(sessionRemover);
await sessionRemover();
}
await Prefs.set("last-run", new Date());
await filters();
await getManager();
})().catch(ex => {

View File

@ -19,6 +19,7 @@ export interface MessageSender {
export interface Tab {
id?: number;
incognito?: boolean;
}
export interface MenuClickInfo {
@ -39,14 +40,72 @@ export interface RawPort {
postMessage: (message: any) => void;
}
export const {extension} = polyfill;
export const {notifications} = polyfill;
interface WebRequestFilter {
urls?: string[];
}
interface WebRequestListener {
addListener(
callback: Function,
filter: WebRequestFilter,
extraInfoSpec: string[]
): void;
removeListener(callback: Function): void;
}
type Header = {name: string; value: string};
export interface DownloadOptions {
conflictAction: string;
filename: string;
saveAs: boolean;
url: string;
method?: string;
body?: string;
incognito?: boolean;
headers: Header[];
}
export interface DownloadsQuery {
id?: number;
}
interface Downloads {
download(download: DownloadOptions): Promise<number>;
open(manId: number): Promise<void>;
show(manId: number): Promise<void>;
pause(manId: number): Promise<void>;
resume(manId: number): Promise<void>;
cancel(manId: number): Promise<void>;
erase(query: DownloadsQuery): Promise<void>;
search(query: DownloadsQuery): Promise<any[]>;
getFileIcon(id: number, options?: any): Promise<string>;
setShelfEnabled(state: boolean): void;
removeFile(manId: number): Promise<void>;
onCreated: ExtensionListener;
onChanged: ExtensionListener;
onErased: ExtensionListener;
}
interface WebRequest {
onBeforeSendHeaders: WebRequestListener;
onSendHeaders: WebRequestListener;
onHeadersReceived: WebRequestListener;
}
export const {browserAction} = polyfill;
export const {contextMenus} = polyfill;
export const {downloads} = polyfill;
export const {downloads}: {downloads: Downloads} = polyfill;
export const {extension} = polyfill;
export const {history} = polyfill;
export const {menus} = polyfill;
export const {notifications} = polyfill;
export const {runtime} = polyfill;
export const {sessions} = polyfill;
export const {storage} = polyfill;
export const {tabs} = polyfill;
export const {webNavigation} = polyfill;
export const {webRequest}: {webRequest: WebRequest} = polyfill;
export const {windows} = polyfill;
export const CHROME = navigator.appVersion.includes("Chrome/");

230
lib/cdheaderparser.ts Normal file
View File

@ -0,0 +1,230 @@
/**
* (c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* eslint-disable max-len,no-magic-numbers */
// License: MPL-2
/**
* This typescript port was done by Nils Maier based on
* https://github.com/Rob--W/open-in-browser/blob/83248155b633ed41bc9cdb1205042653e644abd2/extension/content-disposition.js
* Special thanks goes to Rob doing all the heavy lifting and putting
* it together in a reuseable, open source'd library.
*/
const R_RFC6266 = /(?:^|;)\s*filename\*\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
const R_RFC5987 = /(?:^|;)\s*filename\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
function unquoteRFC2616(value: string) {
if (!value.startsWith("\"")) {
return value;
}
const parts = value.slice(1).split("\\\"");
// Find the first unescaped " and terminate there.
for (let i = 0; i < parts.length; ++i) {
const quotindex = parts[i].indexOf("\"");
if (quotindex !== -1) {
parts[i] = parts[i].slice(0, quotindex);
// Truncate and stop the iteration.
parts.length = i + 1;
}
parts[i] = parts[i].replace(/\\(.)/g, "$1");
}
value = parts.join("\"");
return value;
}
export class CDHeaderParser {
private needsFixup: boolean;
// We need to keep this per instance, because of the global flag.
// Hence we need to reset it after a use.
private R_MULTI = /(?:^|;)\s*filename\*((?!0\d)\d+)(\*?)\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/gi;
/**
* Parse a content-disposition header, with relaxed spec tolerance
*
* @param {string} header Header to parse
* @returns {string} Parsed header
*/
parse(header: string) {
this.needsFixup = true;
// filename*=ext-value ("ext-value" from RFC 5987, referenced by RFC 6266).
{
const match = R_RFC6266.exec(header);
if (match) {
const [, tmp] = match;
let filename = unquoteRFC2616(tmp);
filename = unescape(filename);
filename = this.decodeRFC5897(filename);
filename = this.decodeRFC2047(filename);
return this.maybeFixupEncoding(filename);
}
}
// Continuations (RFC 2231 section 3, referenced by RFC 5987 section 3.1).
// filename*n*=part
// filename*n=part
{
const tmp = this.getParamRFC2231(header);
if (tmp) {
// RFC 2047, section
const filename = this.decodeRFC2047(tmp);
return this.maybeFixupEncoding(filename);
}
}
// filename=value (RFC 5987, section 4.1).
{
const match = R_RFC5987.exec(header);
if (match) {
const [, tmp] = match;
let filename = unquoteRFC2616(tmp);
filename = this.decodeRFC2047(filename);
return this.maybeFixupEncoding(filename);
}
}
return "";
}
private maybeDecode(encoding: string, value: string) {
if (!encoding) {
return value;
}
const bytes = Array.from(value, c => c.charCodeAt(0));
if (!bytes.every(code => code <= 0xff)) {
return value;
}
try {
value = new TextDecoder(encoding, {fatal: true}).
decode(new Uint8Array(bytes));
this.needsFixup = false;
}
catch {
// TextDecoder constructor threw - unrecognized encoding.
}
return value;
}
private maybeFixupEncoding(value: string) {
if (!this.needsFixup && /[\x80-\xff]/.test(value)) {
return value;
}
// Maybe multi-byte UTF-8.
value = this.maybeDecode("utf-8", value);
if (!this.needsFixup) {
return value;
}
// Try iso-8859-1 encoding.
return this.maybeDecode("iso-8859-1", value);
}
private getParamRFC2231(value: string) {
const matches: string[][] = [];
// Iterate over all filename*n= and filename*n*= with n being an integer
// of at least zero. Any non-zero number must not start with '0'.
let match;
this.R_MULTI.lastIndex = 0;
while ((match = this.R_MULTI.exec(value)) !== null) {
const [, num, quot, part] = match;
const n = parseInt(num, 10);
if (n in matches) {
// Ignore anything after the invalid second filename*0.
if (n === 0) {
break;
}
continue;
}
matches[n] = [quot, part];
}
const parts: string[] = [];
for (let n = 0; n < matches.length; ++n) {
if (!(n in matches)) {
// Numbers must be consecutive. Truncate when there is a hole.
break;
}
const [quot, rawPart] = matches[n];
let part = unquoteRFC2616(rawPart);
if (quot) {
part = unescape(part);
if (n === 0) {
part = this.decodeRFC5897(part);
}
}
parts.push(part);
}
return parts.join("");
}
private decodeRFC2047(value: string) {
// RFC 2047-decode the result. Firefox tried to drop support for it, but
// backed out because some servers use it - https://bugzil.la/875615
// Firefox's condition for decoding is here:
// eslint-disable-next-line max-len
// https://searchfox.org/mozilla-central/rev/4a590a5a15e35d88a3b23dd6ac3c471cf85b04a8/netwerk/mime/nsMIMEHeaderParamImpl.cpp#742-748
// We are more strict and only recognize RFC 2047-encoding if the value
// starts with "=?", since then it is likely that the full value is
// RFC 2047-encoded.
// Firefox also decodes words even where RFC 2047 section 5 states:
// "An 'encoded-word' MUST NOT appear within a 'quoted-string'."
// eslint-disable-next-line no-control-regex
if (!value.startsWith("=?") || /[\x00-\x19\x80-\xff]/.test(value)) {
return value;
}
// RFC 2047, section 2.4
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
// charset = token (but let's restrict to characters that denote a
// possibly valid encoding).
// encoding = q or b
// encoded-text = any printable ASCII character other than ? or space.
// ... but Firefox permits ? and space.
return value.replace(
/=\?([\w-]*)\?([QqBb])\?((?:[^?]|\?(?!=))*)\?=/g,
(_, charset, encoding, text) => {
if (encoding === "q" || encoding === "Q") {
// RFC 2047 section 4.2.
text = text.replace(/_/g, " ");
text = text.replace(/=([0-9a-fA-F]{2})/g,
(_: string, hex: string) => String.fromCharCode(parseInt(hex, 16)));
return this.maybeDecode(charset, text);
}
// else encoding is b or B - base64 (RFC 2047 section 4.1)
try {
text = atob(text);
}
catch {
// ignored
}
return this.maybeDecode(charset, text);
});
}
private decodeRFC5897(extValue: string) {
// Decodes "ext-value" from RFC 5987.
const extEnd = extValue.indexOf("'");
if (extEnd < 0) {
// Some servers send "filename*=" without encoding'language' prefix,
// e.g. in https://github.com/Rob--W/open-in-browser/issues/26
// Let's accept the value like Firefox (57) (Chrome 62 rejects it).
return extValue;
}
const encoding = extValue.slice(0, extEnd);
const langvalue = extValue.slice(extEnd + 1);
// Ignore language (RFC 5987 section 3.2.1, and RFC 6266 section 4.1 ).
return this.maybeDecode(encoding, langvalue.replace(/^[^']*'/, ""));
}
}

View File

@ -2,6 +2,9 @@
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
// eslint-disable-next-line no-unused-vars
import { Download } from "./manager/download";
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
// License: MIT
@ -69,7 +72,7 @@ export const DB = new class DB {
return await new Promise(this.getAllInternal);
}
saveItemsInternal(items: any[], resolve: Function, reject: Function) {
saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
if (!items || !items.length || !this.db) {
resolve();
return;
@ -83,9 +86,13 @@ export const DB = new class DB {
if (item.private) {
continue;
}
const req = store.put(item.toJSON());
const json = item.toJSON();
if (item.state === RUNNING || item.state === RETRYING) {
json.state = QUEUED;
}
const req = store.put(json);
if (!("dbId" in item) || item.dbId < 0) {
req.onsuccess = () => item.dbId = req.result;
req.onsuccess = () => item.dbId = req.result as number;
}
}
}
@ -94,7 +101,7 @@ export const DB = new class DB {
}
}
async saveItems(items: any[]) {
async saveItems(items: Download[]) {
await this.init();
return await new Promise(this.saveItemsInternal.bind(this, items));
}

View File

@ -9,7 +9,7 @@ import { EventEmitter } from "./events";
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
// eslint-disable-next-line no-unused-vars
import { Overlayable } from "./objectoverlay";
import * as DEFAULT_FILTERS from "../data/filters.json";
import DEFAULT_FILTERS from "../data/filters.json";
import { FASTFILTER } from "./recentlist";
import { _, locale } from "./i18n";
// eslint-disable-next-line no-unused-vars
@ -94,7 +94,7 @@ function *parseIntoRegexpInternal(str: string): Iterable<RegExp> {
// multi-expression
if (str.includes(",")) {
for (const part in str.split(",")) {
for (const part of str.split(",")) {
yield *parseIntoRegexpInternal(part);
}
return;

View File

@ -2,6 +2,19 @@
// License: MIT
import {memoize} from "./memoize";
import langs from "../_locales/all.json";
import { sorted, naturalCaseCompare } from "./sorting";
export const ALL_LANGS = Object.freeze(new Map<string, string>(
sorted(Object.entries(langs), e => {
return [e[1], e[0]];
}, naturalCaseCompare)));
let CURRENT = "en";
export function getCurrentLanguage() {
return CURRENT;
}
declare let browser: any;
declare let chrome: any;
@ -9,6 +22,8 @@ declare let chrome: any;
const CACHE_KEY = "_cached_locales";
const CUSTOM_KEY = "_custom_locale";
const normalizer = /[^A-Za-z0-9_]/g;
interface JSONEntry {
message: string;
placeholders: any;
@ -72,7 +87,7 @@ class Localization {
}
localize(id: string, ...args: any[]) {
const entry = this.strings.get(id);
const entry = this.strings.get(id.replace(normalizer, "_"));
if (!entry) {
return "";
}
@ -119,16 +134,36 @@ function loadCached() {
}
async function loadRawLocales() {
// en is the base locale
// en is the base locale, always to be loaded
// The loader will override string from it with more specific string
// from other locales
const langs = new Set<string>(["en"]);
const ui = (browser.i18n || chrome.i18n).getUILanguage();
langs.add(ui);
if (ui.includes("-")) {
// Try the base too
langs.add(ui.split(/[_-]+/)[0]);
const uiLang: string = (typeof browser !== "undefined" ? browser : chrome).
i18n.getUILanguage();
// Chrome will only look for underscore versions of locale codes,
// while Firefox will look for both.
// So we better normalize the code to the underscore version.
// However, the API seems to always return the dash-version.
// Add all base locales into ascending order of priority,
// starting with the most unspecific base locale, ending
// with the most specific locale.
// e.g. this will transform ["zh", "CN"] -> ["zh", "zh_CN"]
uiLang.split(/[_-]/g).reduce<string[]>((prev, curr) => {
prev.push(curr);
langs.add(prev.join("_"));
return prev;
}, []);
if (CURRENT && CURRENT !== "default") {
langs.delete(CURRENT);
langs.add(CURRENT);
}
const fetched = await Promise.all(Array.from(langs, fetchLanguage));
const valid = Array.from(langs).filter(e => ALL_LANGS.has(e));
const fetched = await Promise.all(Array.from(valid, fetchLanguage));
return fetched.filter(e => !!e);
}
@ -136,6 +171,21 @@ async function load(): Promise<Localization> {
try {
checkBrowser();
try {
let currentLang: any = "";
if (typeof browser !== "undefined") {
currentLang = await browser.storage.sync.get("language");
}
else {
currentLang = await new Promise(
resolve => chrome.storage.sync.get("language", resolve));
}
if ("language" in currentLang) {
currentLang = currentLang.language;
}
if (!currentLang || !currentLang.length) {
currentLang = "default";
}
CURRENT = currentLang;
// en is the base locale
let valid = loadCached();
if (!valid) {
@ -147,7 +197,6 @@ async function load(): Promise<Localization> {
}
const custom = localStorage.getItem(CUSTOM_KEY);
console.log("custom", custom);
if (custom) {
try {
valid.push(JSON.parse(custom));

162
lib/iconcache.ts Normal file
View File

@ -0,0 +1,162 @@
"use strict";
// License: MIT
import { downloads, CHROME } from "./browser";
import { EventEmitter } from "../uikit/lib/events";
import { PromiseSerializer } from "./pserializer";
const VERSION = 1;
const STORE = "iconcache";
// eslint-disable-next-line no-magic-numbers
const CACHE_SIZES = CHROME ? [16, 32] : [16, 32, 64, 127];
const BLACKLISTED = Object.freeze(new Set([
"",
"ext",
"ico",
"pif",
"scr",
"ani",
"cur",
"ttf",
"otf",
"woff",
"woff2",
"cpl",
"desktop",
"app",
]));
async function getIcon(size: number, manId: number) {
const raw = await downloads.getFileIcon(manId, {size});
const icon = new URL(raw);
if (icon.protocol === "data:") {
const res = await fetch(icon.toString());
const blob = await res.blob();
return {size, icon: blob};
}
return {size, icon};
}
const SYNONYMS = Object.freeze(new Map<string, string>([
["jpe", "jpg"],
["jpeg", "jpg"],
["jfif", "jpg"],
["mpe", "mpg"],
["mpeg", "mpg"],
["m4v", "mp4"],
]));
export const IconCache = new class IconCache extends EventEmitter {
private db: Promise<IDBDatabase>;
private cache: Map<string, string>;
constructor() {
super();
this.db = this.init();
this.cache = new Map();
this.get = PromiseSerializer.wrapNew(8, this, this.get);
this.set = PromiseSerializer.wrapNew(1, this, this.set);
}
private async init() {
return await new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open(STORE, VERSION);
req.onupgradeneeded = evt => {
const db = req.result;
switch (evt.oldVersion) {
case 0: {
db.createObjectStore(STORE);
break;
}
}
};
req.onerror = ex => reject(ex);
req.onsuccess = () => {
resolve(req.result);
};
});
}
private normalize(ext: string) {
ext = ext.toLocaleLowerCase();
return SYNONYMS.get(ext) || ext;
}
// eslint-disable-next-line no-magic-numbers
async get(ext: string, size = 16) {
ext = this.normalize(ext);
if (BLACKLISTED.has(ext)) {
return undefined;
}
const sext = `${ext}-${size}`;
let rv = this.cache.get(sext);
if (rv) {
return rv;
}
const db = await this.db;
rv = this.cache.get(sext);
if (rv) {
return rv;
}
return await new Promise<string | undefined>(resolve => {
const trans = db.transaction(STORE, "readonly");
trans.onerror = () => resolve(undefined);
const store = trans.objectStore(STORE);
const req = store.get(sext);
req.onerror = () => resolve(undefined);
req.onsuccess = () => {
const rv = this.cache.get(sext);
if (rv) {
resolve(rv);
return;
}
let {result} = req;
if (!result) {
resolve(undefined);
return;
}
if (typeof req.result !== "string") {
result = URL.createObjectURL(result).toString();
}
this.cache.set(sext, result);
this.cache.set(ext, "");
resolve(result);
};
});
}
async set(ext: string, manId: number) {
ext = this.normalize(ext);
if (BLACKLISTED.has(ext)) {
return;
}
if (this.cache.has(ext)) {
// already processed in this session
return;
}
// eslint-disable-next-line no-magic-numbers
const urls = await Promise.all(CACHE_SIZES.map(
size => getIcon(size, manId)));
if (this.cache.has(ext)) {
// already processed in this session
return;
}
for (const {size, icon} of urls) {
this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon));
}
this.cache.set(ext, "");
const db = await this.db;
await new Promise((resolve, reject) => {
const trans = db.transaction(STORE, "readwrite");
trans.onerror = reject;
trans.oncomplete = resolve;
const store = trans.objectStore(STORE);
for (const {size, icon} of urls) {
store.put(icon, `${ext}-${size}`);
}
});
this.emit("cached", ext);
}
}();

247
lib/imex.ts Normal file
View File

@ -0,0 +1,247 @@
"use strict";
// License: MIT
import { getTextLinks } from "./textlinks";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
import { ALLOWED_SCHEMES } from "./constants";
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
export const NS_DTA = "http://www.downthemall.net/properties#";
function parseNum(
file: Element,
attr: string,
defaultValue: number,
ns = NS_METALINK_RFC5854) {
const val = file.getAttributeNS(ns, attr);
if (!val) {
return defaultValue + 1;
}
const num = parseInt(val, 10);
if (isFinite(num)) {
return num;
}
return defaultValue + 1;
}
function importMeta4(data: string) {
const parser = new DOMParser();
const document = parser.parseFromString(data, "text/xml");
const {documentElement} = document;
const items: BaseItem[] = [];
let batch = 0;
for (const file of documentElement.querySelectorAll("file")) {
try {
const url = Array.from(file.querySelectorAll("url")).map(u => {
try {
const {textContent} = u;
if (!textContent) {
return null;
}
const url = new URL(textContent);
if (!ALLOWED_SCHEMES.has(url.protocol)) {
return null;
}
const prio = parseNum(u, "priority", 0);
return {
url,
prio
};
}
catch {
return null;
}
}).filter(u => !!u).reduce((p, c) => {
if (!c) {
return null;
}
if (!p || p.prio < c.prio) {
return c;
}
return p;
});
if (!url) {
continue;
}
batch = parseNum(file, "num", batch, NS_DTA);
const idx = parseNum(file, "idx", 0, NS_DTA);
const item: BaseItem = {
url: url.url.toString(),
usable: decodeURIComponent(url.url.toString()),
batch,
idx
};
const ref = file.getAttributeNS(NS_DTA, "referrer");
if (ref) {
item.referrer = ref;
item.usableReferrer = decodeURIComponent(ref);
}
const mask = file.getAttributeNS(NS_DTA, "mask");
if (mask) {
item.mask = mask;
}
items.push(item);
}
catch (ex) {
console.error("Failed to import file", ex);
}
}
return items;
}
function parseKV(current: BaseItem, line: string) {
const [k, v] = line.split("=", 2);
switch (k.toLocaleLowerCase().trim()) {
case "referer": {
const rurls = getTextLinks(v);
if (rurls && rurls.length) {
current.referrer = rurls.pop();
current.usableReferrer = decodeURIComponent(current.referrer || "");
}
break;
}
}
}
export function importText(data: string) {
if (data.includes(NS_METALINK_RFC5854)) {
return importMeta4(data);
}
const splitter = /(.+)\n|(.+)$/g;
const spacer = /^\s+/;
let match;
let current: BaseItem | undefined = undefined;
let idx = 0;
const items = [];
while ((match = splitter.exec(data)) !== null) {
try {
const line = match[0].trimRight();
if (!line) {
continue;
}
if (spacer.test(line)) {
if (!current) {
continue;
}
parseKV(current, line);
continue;
}
const urls = getTextLinks(line);
if (!urls || !urls.length) {
continue;
}
current = {
url: urls[0],
usable: decodeURIComponent(urls[0]),
idx: ++idx
};
items.push(current);
}
catch (ex) {
current = undefined;
console.error("Failed to import", ex);
}
}
return items;
}
export interface Exporter {
fileName: string;
getText(items: BaseItem[]): string;
}
class TextExporter {
readonly fileName: string;
constructor() {
this.fileName = "links.txt";
}
getText(items: BaseItem[]) {
const lines = [];
for (const item of items) {
lines.push(item.url);
}
return lines.join("\n");
}
}
class Aria2Exporter {
readonly fileName: string;
constructor() {
this.fileName = "links.aria2.txt";
}
getText(items: BaseItem[]) {
const lines = [];
for (const item of items) {
lines.push(item.url);
if (item.referrer) {
lines.push(` referer=${item.referrer}`);
}
}
return lines.join("\n");
}
}
class MetalinkExporter {
readonly fileName: string;
constructor() {
this.fileName = "links.meta4";
}
getText(items: BaseItem[]) {
const document = window.document.implementation.
createDocument(NS_METALINK_RFC5854, "metalink", null);
const root = document.documentElement;
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
root.appendChild(document.createComment(
"metalink as exported by DownThemAll!",
));
for (const item of items) {
const aitem = item as any;
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
f.setAttribute("name", aitem.currentName);
if (item.batch) {
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
}
if (item.idx) {
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
}
if (item.referrer) {
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
}
if (item.mask) {
f.setAttributeNS(NS_DTA, "mask", item.mask);
}
if (item.description) {
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
n.textContent = item.description;
f.appendChild(n);
}
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
u.textContent = item.url;
f.appendChild(u);
if (aitem.totalSize > 0) {
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
s.textContent = aitem.totalSize.toString();
f.appendChild(s);
}
root.appendChild(f);
}
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
xml += root.outerHTML;
return xml;
}
}
export const textExporter = new TextExporter();
export const aria2Exporter = new Aria2Exporter();
export const metalinkExporter = new MetalinkExporter();

4
lib/ipreg.ts Normal file
View File

@ -0,0 +1,4 @@
"use strict";
// License: MIT
export const IPReg = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$|^(?:(?:(?:[0-9a-fA-F]{1,4}):){7}(?:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){6}(?:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){5}(?::((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,2}|:)|(?:(?:[0-9a-fA-F]{1,4}):){4}(?:(:(?:[0-9a-fA-F]{1,4})){0,1}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,3}|:)|(?:(?:[0-9a-fA-F]{1,4}):){3}(?:(:(?:[0-9a-fA-F]{1,4})){0,2}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,4}|:)|(?:(?:[0-9a-fA-F]{1,4}):){2}(?:(:(?:[0-9a-fA-F]{1,4})){0,3}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,5}|:)|(?:(?:[0-9a-fA-F]{1,4}):){1}(?:(:(?:[0-9a-fA-F]{1,4})){0,4}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,6}|:)|(?::((?::(?:[0-9a-fA-F]{1,4})){0,5}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?::(?:[0-9a-fA-F]{1,4})){1,7}|:)))(%[0-9a-zA-Z]{1,})?$/;

View File

@ -15,6 +15,7 @@ export interface BaseItem {
batch?: number;
idx: number;
mask?: string;
subfolder?: string;
startDate?: number;
private?: boolean;
postData?: string;
@ -27,6 +28,7 @@ const OPTIONPROPS = Object.freeze([
"fileName",
"batch", "idx",
"mask",
"subfolder",
"startDate",
"private",
"postData",

View File

@ -5,6 +5,8 @@
import { parsePath, URLd } from "../util";
import { QUEUED, RUNNING, PAUSED } from "./state";
import Renamer from "./renamer";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../item";
const SAVEDPROPS = [
"state",
@ -14,6 +16,7 @@ const SAVEDPROPS = [
"usableReferrer",
"fileName",
"mask",
"subfolder",
"date",
// batches
"batch",
@ -27,6 +30,9 @@ const SAVEDPROPS = [
"written",
// server stuff
"serverName",
"browserName",
"mime",
"prerolled",
// other options
"private",
// db
@ -39,10 +45,15 @@ const DEFAULTS = {
state: QUEUED,
error: "",
serverName: "",
browserName: "",
fileName: "",
totalSize: 0,
written: 0,
manId: 0,
mime: "",
prerolled: false,
retries: 0,
deadline: 0
};
let sessionId = 0;
@ -59,14 +70,26 @@ export class BaseDownload {
public url: string;
public usable: string;
public uReferrer: URLd;
public referrer: string;
public usableReferrer: string;
public startDate: Date;
public fileName: string;
public description?: string;
public title?: string;
public batch: number;
public idx: number;
public error: string;
public postData: any;
@ -79,10 +102,19 @@ export class BaseDownload {
public serverName: string;
public browserName: string;
public mime: string;
public mask: string;
public subfolder: string;
constructor(options: any) {
public prerolled: boolean;
public retries: number;
constructor(options: BaseItem) {
Object.assign(this, DEFAULTS);
this.assign(options);
if (this.state === RUNNING) {
@ -90,14 +122,16 @@ export class BaseDownload {
}
this.sessionId = ++sessionId;
this.renamer = new Renamer(this);
this.retries = 0;
}
assign(options: any) {
assign(options: BaseItem) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this;
const other: any = options;
for (const prop of SAVEDPROPS) {
if (prop in options) {
self[prop] = options[prop];
self[prop] = other[prop];
}
}
this.uURL = new URL(this.url) as URLd;
@ -115,6 +149,10 @@ export class BaseDownload {
return this.serverName || this.fileName || this.urlName || "index.html";
}
get currentName() {
return this.browserName || this.dest.name || this.finalName;
}
get urlName() {
const path = parsePath(this.uURL);
if (path.name) {
@ -152,7 +190,10 @@ export class BaseDownload {
rv.destName = dest.name;
rv.destPath = dest.path;
rv.destFull = dest.full;
rv.currentName = this.browserName || rv.destName || rv.finalName;
rv.error = this.error;
rv.ext = this.renamer.p_ext;
rv.retries = this.retries;
return rv;
}
}

View File

@ -1,35 +1,42 @@
"use strict";
// License: MIT
import { Prefs } from "../prefs";
import { parsePath, filterInSitu } from "../util";
import {
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
FORCABLE, PAUSABLE, CANCELABLE,
} from "./state";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars
import { CHROME, downloads, DownloadOptions } from "../browser";
import { Prefs, PrefWatcher } from "../prefs";
import { PromiseSerializer } from "../pserializer";
import { filterInSitu, parsePath } from "../util";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars
import { Manager } from "./man";
import { downloads } from "../browser";
import Renamer from "./renamer";
import {
CANCELABLE,
CANCELED,
DONE,
FORCABLE,
MISSING,
PAUSABLE,
PAUSED,
QUEUED,
RUNNING,
RETRYING
} from "./state";
import { Preroller } from "./preroller";
function isRecoverable(error: string) {
switch (error) {
case "SERVER_FAILED":
return true;
const setShelfEnabled = downloads.setShelfEnabled || function() {
// ignored
};
type Header = {name: string; value: string};
interface Options {
conflictAction: string;
filename: string;
saveAs: boolean;
url: string;
method?: string;
body?: string;
incognito: boolean;
headers: Header[];
default:
return error.startsWith("NETWORK_");
}
}
const RETRIES = new PrefWatcher("retries", 5);
const RETRY_TIME = new PrefWatcher("retry-time", 5);
export class Download extends BaseDownload {
public manager: Manager;
@ -41,6 +48,10 @@ export class Download extends BaseDownload {
public error: string;
public dbId: number;
public deadline: number;
constructor(manager: Manager, options: any) {
super(options);
this.manager = manager;
@ -50,6 +61,7 @@ export class Download extends BaseDownload {
}
markDirty() {
this.renamer = new Renamer(this);
this.manager.setDirty(this);
}
@ -71,22 +83,28 @@ export class Download extends BaseDownload {
if (this.manId) {
const {manId: id} = this;
try {
const state = await downloads.search({id});
if (state[0].state === "in_progress") {
const state = (await downloads.search({id})).pop() || {};
if (state.state === "in_progress" && !state.error && !state.paused) {
this.changeState(RUNNING);
this.updateStateFromBrowser();
return;
}
if (!state[0].canResume) {
if (state.state === "complete") {
this.changeState(DONE);
this.updateStateFromBrowser();
return;
}
if (!state.canResume) {
throw new Error("Cannot resume");
}
// Cannot await here
// Firefox bug: will not return until download is finished
downloads.resume(id).catch(() => {});
downloads.resume(id).catch(console.error);
this.changeState(RUNNING);
return;
}
catch (ex) {
console.error("cannot resume", ex);
this.manager.removeManId(this.manId);
this.removeFromBrowser();
}
@ -94,49 +112,64 @@ export class Download extends BaseDownload {
if (this.state !== QUEUED) {
throw new Error("invalid state");
}
console.trace("starting", this.toString(), this.toMsg());
console.log("starting", this.toString(), this.toMsg());
this.changeState(RUNNING);
// Do NOT await
this.reallyStart();
}
private async reallyStart() {
try {
const options: Options = {
if (!this.prerolled) {
await this.maybePreroll();
if (this.state !== RUNNING) {
// Aborted by preroll
return;
}
}
const options: DownloadOptions = {
conflictAction: await Prefs.get("conflict-action"),
filename: this.dest.full,
saveAs: false,
url: this.url,
headers: [],
incognito: this.private
};
if (!CHROME && this.private) {
options.incognito = true;
}
if (this.postData) {
options.body = this.postData;
options.method = "POST";
}
if (this.referrer) {
if (!CHROME && this.referrer) {
options.headers.push({
name: "Referer",
value: this.referrer
});
}
else if (CHROME) {
options.headers.push({
name: "X-DTA-ID",
value: this.sessionId.toString(),
});
}
if (this.manId) {
this.manager.removeManId(this.manId);
}
setShelfEnabled(false);
try {
try {
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
catch (ex) {
if (!this.referrer) {
throw ex;
}
// Re-attempt without referrer
filterInSitu(options.headers, h => h.name !== "Referer");
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
finally {
setShelfEnabled(true);
catch (ex) {
if (!this.referrer) {
throw ex;
}
// Re-attempt without referrer
filterInSitu(options.headers, h => h.name !== "Referer");
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
this.markDirty();
}
@ -147,6 +180,41 @@ export class Download extends BaseDownload {
}
}
private async maybePreroll() {
try {
if (this.prerolled) {
// Check again, just in case, async and all
return;
}
const roller = new Preroller(this);
if (!roller.shouldPreroll) {
return;
}
const res = await roller.roll();
if (!res) {
return;
}
if (res.mime) {
this.mime = res.mime;
}
if (res.name) {
this.serverName = res.name;
}
if (res.error) {
this.cancelAccordingToError(res.error);
}
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
finally {
if (this.state === RUNNING) {
this.prerolled = true;
this.markDirty();
}
}
}
resume(forced = false) {
if (!(FORCABLE & this.state)) {
return;
@ -159,26 +227,41 @@ export class Download extends BaseDownload {
}
}
async pause() {
async pause(retry?: boolean) {
if (!(PAUSABLE & this.state)) {
return;
}
if (!retry) {
this.retries = 0;
this.deadline = 0;
}
else {
// eslint-disable-next-line no-magic-numbers
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
}
if (this.state === RUNNING && this.manId) {
try {
await downloads.pause(this.manId);
}
catch (ex) {
console.error("pause", ex.toString(), ex);
this.cancel();
return;
}
}
this.changeState(PAUSED);
this.changeState(retry ? RETRYING : PAUSED);
}
reset() {
this.prerolled = false;
this.manId = 0;
this.written = this.totalSize = 0;
this.serverName = "";
this.mime = this.serverName = this.browserName = "";
this.retries = 0;
this.deadline = 0;
}
async removeFromBrowser() {
@ -211,6 +294,17 @@ export class Download extends BaseDownload {
this.changeState(CANCELED);
}
async cancelAccordingToError(error: string) {
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
this.cancel();
this.error = error;
return;
}
await this.pause(true);
this.error = error;
}
setMissing() {
if (this.manId) {
this.manager.removeManId(this.manId);
@ -255,14 +349,19 @@ export class Download extends BaseDownload {
const state = (await downloads.search({id: this.manId})).pop();
const {filename, error} = state;
const path = parsePath(filename);
this.serverName = path.name;
this.browserName = path.name;
this.adoptSize(state);
if (!this.mime && state.mime) {
this.mime = state.mime;
}
this.markDirty();
switch (state.state) {
case "in_progress":
if (error) {
this.cancel();
this.error = error;
if (state.paused) {
this.changeState(PAUSED);
}
else if (error) {
this.cancelAccordingToError(error);
}
else {
this.changeState(RUNNING);
@ -273,6 +372,9 @@ export class Download extends BaseDownload {
if (state.paused) {
this.changeState(PAUSED);
}
else if (error) {
this.cancelAccordingToError(error);
}
else {
this.cancel();
this.error = error || "";

View File

@ -4,30 +4,39 @@
import { EventEmitter } from "../events";
import { Notification } from "../notifications";
import { DB } from "../db";
import { QUEUED, CANCELED, RUNNING } from "./state";
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
// eslint-disable-next-line no-unused-vars
import { Bus, Port } from "../bus";
import { sort } from "../sorting";
import { Prefs } from "../prefs";
import { Prefs, PrefWatcher } from "../prefs";
import { _ } from "../i18n";
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
import { PromiseSerializer } from "../pserializer";
import {Download} from "./download";
import {ManagerPort} from "./port";
import {Scheduler} from "./scheduler";
import {Limits} from "./limits";
import { downloads } from "../browser";
import { Download } from "./download";
import { ManagerPort } from "./port";
import { Scheduler } from "./scheduler";
import { Limits } from "./limits";
import { downloads, runtime, webRequest, CHROME } from "../browser";
const US = runtime.getURL("");
const AUTOSAVE_TIMEOUT = 2000;
const DIRTY_TIMEOUT = 100;
// eslint-disable-next-line no-magic-numbers
const MISSING_TIMEOUT = 12 * 1000;
const RELOAD_TIMEOUT = 10 * 1000;
const setShelfEnabled = downloads.setShelfEnabled || function() {
// ignored
};
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
const SOUNDS = new PrefWatcher("sounds", false);
export class Manager extends EventEmitter {
private items: Download[];
private active: boolean;
public active: boolean;
private notifiedFinished: boolean;
@ -43,22 +52,31 @@ export class Manager extends EventEmitter {
private readonly running: Set<Download>;
private readonly retrying: Set<Download>;
private scheduler: Scheduler | null;
private shouldReload: boolean;
private deadlineTimer: number;
constructor() {
super();
this.active = true;
this.shouldReload = false;
this.notifiedFinished = true;
this.items = [];
this.saveQueue = new CoalescedUpdate(
AUTOSAVE_TIMEOUT, this.save.bind(this));
this.dirty = new CoalescedUpdate(
DIRTY_TIMEOUT, this.processDirty.bind(this));
this.processDeadlines = this.processDeadlines.bind(this);
this.sids = new Map();
this.manIds = new Map();
this.ports = new Set();
this.scheduler = null;
this.running = new Set();
this.retrying = new Set();
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
@ -75,6 +93,14 @@ export class Manager extends EventEmitter {
Limits.on("changed", () => {
this.resetScheduler();
});
if (CHROME) {
webRequest.onBeforeSendHeaders.addListener(
this.stuffReferrer.bind(this),
{urls: ["<all_urls>"]},
["blocking", "requestHeaders", "extraHeaders"]
);
}
}
async init() {
@ -88,9 +114,19 @@ export class Manager extends EventEmitter {
}
this.items.push(rv);
});
await this.resetScheduler();
// Do not wait for the scheduler
this.resetScheduler();
this.emit("inited");
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
runtime.onUpdateAvailable.addListener(() => {
if (this.running.size) {
this.shouldReload = true;
return;
}
runtime.reload();
});
return this;
}
@ -136,7 +172,7 @@ export class Manager extends EventEmitter {
}
const next = await this.scheduler.next(this.running);
if (!next) {
this.maybeNotifyFinished();
this.maybeRunFinishActions();
break;
}
if (this.running.has(next) || next.state !== QUEUED) {
@ -156,19 +192,43 @@ export class Manager extends EventEmitter {
async startDownload(download: Download) {
// Add to running first, so we don't confuse the scheduler and other parts
this.running.add(download);
setShelfEnabled(false);
await download.start();
this.notifiedFinished = false;
}
async maybeNotifyFinished() {
if (!(await Prefs.get("finish-notification"))) {
maybeRunFinishActions() {
if (this.running.size) {
return;
}
if (this.notifiedFinished || this.running.size) {
this.maybeNotifyFinished();
if (this.shouldReload) {
this.saveQueue.trigger();
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
setShelfEnabled(true);
}
maybeNotifyFinished() {
if (this.notifiedFinished || this.running.size || this.retrying.size) {
return;
}
if (SOUNDS.value) {
const audio = new Audio(runtime.getURL("/style/done.opus"));
audio.addEventListener("canplaythrough", () => audio.play());
audio.addEventListener("ended", () => document.body.removeChild(audio));
audio.addEventListener("error", () => document.body.removeChild(audio));
document.body.appendChild(audio);
}
if (FINISH_NOTIFICATION.value) {
new Notification(null, _("queue-finished"));
}
this.notifiedFinished = true;
new Notification(null, _("queue-finished"));
}
addManId(id: number, download: Download) {
@ -216,7 +276,7 @@ export class Manager extends EventEmitter {
this.emit("dirty", items);
}
save(items: Download[]) {
private save(items: Download[]) {
DB.saveItems(items.filter(i => !i.removed)).
catch(console.error);
}
@ -267,6 +327,10 @@ export class Manager extends EventEmitter {
if (oldState === RUNNING) {
this.running.delete(download);
}
else if (oldState === RETRYING) {
this.retrying.delete(download);
this.findDeadline();
}
if (newState === QUEUED) {
this.resetScheduler();
this.startNext().catch(console.error);
@ -278,10 +342,56 @@ export class Manager extends EventEmitter {
this.running.add(download);
}
else {
if (newState === RETRYING) {
this.addRetry(download);
}
this.startNext().catch(console.error);
}
}
addRetry(download: Download) {
this.retrying.add(download);
this.findDeadline();
}
private findDeadline() {
let deadline = Array.from(this.retrying).
reduce<number>((deadline, item) => {
if (deadline) {
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
}
return item.deadline;
}, 0);
if (deadline <= 0) {
return;
}
deadline -= Date.now();
if (deadline <= 0) {
return;
}
if (this.deadlineTimer) {
window.clearTimeout(this.deadlineTimer);
}
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
}
private processDeadlines() {
this.deadlineTimer = 0;
try {
const now = Date.now();
this.items.forEach(item => {
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
this.retrying.delete(item);
item.resume(false);
}
});
}
finally {
this.findDeadline();
}
}
sorted(sids: number[]) {
try {
// Construct new items
@ -341,6 +451,35 @@ export class Manager extends EventEmitter {
}
this.emit("active", this.active);
}
getMsgItems() {
return this.items.map(e => e.toMsg());
}
stuffReferrer(details: any): any {
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
return undefined;
}
const sidx = details.requestHeaders.findIndex(
(e: any) => e.name.toLowerCase() === "x-dta-id");
if (sidx < 0) {
return undefined;
}
const sid = parseInt(details.requestHeaders[sidx].value, 10);
details.requestHeaders.splice(sidx, 1);
const item = this.sids.get(sid);
if (!item) {
return undefined;
}
details.requestHeaders.push({
name: "Referer",
value: (item.uReferrer || item.uURL).toString()
});
const rv: any = {
requestHeaders: details.requestHeaders
};
return rv;
}
}
let inited: Promise<Manager>;

View File

@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
import { API } from "../api";
// eslint-disable-next-line no-unused-vars
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars
import { Manager } from "./man";
// eslint-disable-next-line no-unused-vars
import { Port } from "../bus";
type SID = {sid: number};
type SIDS = {
@ -13,9 +17,9 @@ type SIDS = {
};
export class ManagerPort {
private manager: any;
private manager: Manager;
private port: any;
private port: Port;
constructor(manager: any, port: any) {
this.manager = manager;
@ -61,6 +65,7 @@ export class ManagerPort {
delete this.manager;
delete this.port;
});
this.port.post("active", this.manager.active);
this.sendAll();
}
@ -78,7 +83,6 @@ export class ManagerPort {
}
sendAll() {
this.port.post(
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
this.port.post("all", this.manager.getMsgItems());
}
}

234
lib/manager/preroller.ts Normal file
View File

@ -0,0 +1,234 @@
"use strict";
// License: MIT
import MimeType from "whatwg-mimetype";
// eslint-disable-next-line no-unused-vars
import { Download } from "./download";
import { CHROME, webRequest } from "../browser";
import { CDHeaderParser } from "../cdheaderparser";
import { sanitizePath, parsePath } from "../util";
import { MimeDB } from "../mime";
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
const PREROLL_HOSTS = /4cdn|chan/;
const PREROLL_TIMEOUT = 10000;
const PREROLL_NOPE = new Set<string>();
/* eslint-disable no-magic-numbers */
const NOPE_STATUSES = Object.freeze(new Set([
400,
401,
402,
405,
416,
]));
/* eslint-enable no-magic-numbers */
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
"php",
"asp",
"aspx",
"inc",
"py",
"pl",
"action",
"htm",
"html",
"shtml"
]));
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
const CDPARSER = new CDHeaderParser();
export interface PrerollResults {
error?: string;
name?: string;
mime?: string;
finalURL?: string;
}
export class Preroller {
private readonly download: Download
constructor(download: Download) {
this.download = download;
}
get shouldPreroll() {
const {uURL, renamer} = this.download;
const {pathname, search, host} = uURL;
if (PREROLL_NOPE.has(host)) {
return false;
}
if (!renamer.p_ext) {
return true;
}
if (search.length) {
return true;
}
if (uURL.pathname.endsWith("/")) {
return true;
}
if (PREROLL_HEURISTICS.test(pathname)) {
return true;
}
if (PREROLL_HOSTS.test(host)) {
return true;
}
return false;
}
async roll() {
try {
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
return null;
}
private async prerollFirefox() {
const controller = new AbortController();
const {signal} = controller;
const {uURL, uReferrer} = this.download;
const res = await fetch(uURL.toString(), {
method: "GET",
headers: new Headers({
Range: "bytes=0-1",
}),
mode: "same-origin",
signal,
referrer: (uReferrer || uURL).toString(),
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const {headers} = res;
return this.finalize(headers, res);
}
private async prerollChrome() {
let rid = "";
const {uURL, uReferrer} = this.download;
const rurl = uURL.toString();
let listener: any;
const wr = new Promise<any[]>(resolve => {
listener = (details: any) => {
const {url, requestId, statusCode} = details;
if (rid !== requestId && url !== rurl) {
return;
}
// eslint-disable-next-line no-magic-numbers
if (statusCode >= 300 && statusCode < 400) {
// Redirect, continue tracking;
rid = requestId;
return;
}
resolve(details.responseHeaders);
};
webRequest.onHeadersReceived.addListener(
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
});
const p = Promise.race([
wr,
new Promise<any[]>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
]);
p.finally(() => {
webRequest.onHeadersReceived.removeListener(listener);
});
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(rurl, {
method: "GET",
headers: new Headers({
"Range": "bytes=0-1",
"X-DTA-ID": this.download.sessionId.toString(),
}),
signal,
referrer: (uReferrer || uURL).toString(),
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const headers = await p;
return this.finalize(
new Headers(headers.map(i => [i.name, i.value])), res);
}
private finalize(headers: Headers, res: Response): PrerollResults {
const rv: PrerollResults = {};
const type = MimeType.parse(headers.get("content-type") || "");
if (type) {
rv.mime = type.essence;
}
const {p_ext: ext} = this.download.renamer;
const dispHeader = headers.get("content-disposition");
if (dispHeader) {
const file = CDPARSER.parse(dispHeader);
// Sanitize
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
}
else if (!ext || PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
const {searchParams} = this.download.uURL;
let detected = "";
for (const [, value] of searchParams) {
if (!NAME_TESTER.test(value)) {
continue;
}
const p = parsePath(value);
if (!p.base || !p.ext) {
continue;
}
if (!MimeDB.hasExtension(p.ext)) {
continue;
}
if (rv.mime) {
const mime = MimeDB.getMime(rv.mime);
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
continue;
}
}
const sanitized = sanitizePath(p.name);
if (sanitized.length <= detected.length) {
continue;
}
detected = sanitized;
}
if (detected) {
rv.name = detected;
}
}
rv.finalURL = res.url;
/* eslint-disable no-magic-numbers */
const {status} = res;
if (status === 404) {
rv.error = "SERVER_BAD_CONTENT";
}
else if (status === 403) {
rv.error = "SERVER_FORBIDDEN";
}
else if (status === 402 || status === 407) {
rv.error = "SERVER_UNAUTHORIZED";
}
else if (NOPE_STATUSES.has(status)) {
PREROLL_NOPE.add(this.download.uURL.host);
if (PREROLL_NOPE.size > 1000) {
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
}
}
else if (status > 400 && status < 500) {
rv.error = "SERVER_FAILED";
}
/* eslint-enable no-magic-numbers */
return rv;
}
}

View File

@ -2,8 +2,12 @@
"use strict";
// License: MIT
import { parsePath, sanitizePath } from "../util";
import { _ } from "../i18n";
import { MimeDB } from "../mime";
// eslint-disable-next-line no-unused-vars
import { parsePath, PathInfo, sanitizePath } from "../util";
// eslint-disable-next-line no-unused-vars
import { BaseDownload } from "./basedownload";
const REPLACE_EXPR = /\*\w+\*/gi;
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
});
export default class Renamer {
private readonly d: any;
private readonly d: BaseDownload;
constructor(download: any) {
private readonly nameinfo: PathInfo;
constructor(download: BaseDownload) {
this.d = download;
const info = parsePath(this.d.finalName);
this.nameinfo = this.fixupExtension(info);
}
get nameinfo() {
return parsePath(this.d.finalName);
private fixupExtension(info: PathInfo): PathInfo {
if (!this.d.mime) {
return info;
}
const mime = MimeDB.getMime(this.d.mime);
if (!mime) {
return info;
}
const {ext} = info;
if (mime.major === "image" || mime.major === "video") {
if (ext && mime.extensions.has(ext.toLowerCase())) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
}
if (ext) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
}
get ref() {
return this.d.uReferrer;
}
get p_name() {
return this.nameinfo.base;
}
@ -169,24 +193,24 @@ export default class Renamer {
}
toString() {
const {mask} = this.d;
const {mask, subfolder} = this.d;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this;
// XXX flat
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
let prop = type.substr(1, type.length - 2);
const flat = prop.startsWith("flat");
if (flat) {
prop = prop.substr(4);
}
prop = `p_${prop}`;
const rv = (prop in self) ?
let rv = (prop in self) ?
(self[prop] || "").trim() :
type;
if (flat) {
return rv.replace(/\/+/g, "-");
rv = rv.replace(/[/\\]+/g, "-");
}
return rv;
return rv.replace(/\/{2,}/g, "/");
}));
}
}

View File

@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
export const DONE = 1 << 4;
export const CANCELED = 1 << 5;
export const MISSING = 1 << 6;
export const RETRYING = 1 << 7;
export const RESUMABLE = PAUSED | CANCELED;
export const FORCABLE = PAUSED | QUEUED | CANCELED;
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
export const PAUSABLE = QUEUED | CANCELED | RUNNING | RETRYING;
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;

65
lib/mime.ts Normal file
View File

@ -0,0 +1,65 @@
"use strict";
// License: MIT
import mime from "../data/mime.json";
export class MimeInfo {
public readonly type: string;
public readonly extensions: Set<string>;
public readonly major: string;
public readonly minor: string;
public readonly primary: string;
constructor(type: string, extensions: string[]) {
this.type = type;
const [major, minor] = type.split("/", 2);
this.major = major;
this.minor = minor;
[this.primary] = extensions;
this.extensions = new Set(extensions);
Object.freeze(this);
}
}
export const MimeDB = new class MimeDB {
private readonly mimeToExts: Map<string, MimeInfo>;
private readonly registeredExtensions: Set<string>;
constructor() {
const exts = new Map<string, string[]>();
for (const [prim, more] of Object.entries(mime.e)) {
let toadd = more;
if (!Array.isArray(toadd)) {
toadd = [toadd];
}
toadd.unshift(prim);
exts.set(prim, toadd);
}
this.mimeToExts = new Map(Array.from(
Object.entries(mime.m),
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
));
const all = Array.from(
this.mimeToExts.values(),
m => Array.from(m.extensions, e => e.toLowerCase()));
this.registeredExtensions = new Set(all.flat());
}
getPrimary(mime: string) {
const info = this.mimeToExts.get(mime.trim().toLocaleLowerCase());
return info ? info.primary : "";
}
getMime(mime: string) {
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
}
hasExtension(ext: string) {
return this.registeredExtensions.has(ext.toLowerCase());
}
}();

View File

@ -12,13 +12,20 @@ const DEFAULTS = {
message: "message",
};
const TIMEOUT = 4000;
let gid = 1;
export class Notification extends EventEmitter {
private notification: any;
private readonly generated: boolean;
constructor(id: string | null, options = {}) {
super();
id = id || "DownThemAll-notification";
this.generated = !id;
id = id || `DownThemAll-notification${++gid}`;
if (typeof options === "string") {
options = {message: options};
}
@ -39,11 +46,16 @@ export class Notification extends EventEmitter {
opened(notification: any) {
this.notification = notification;
this.emit("opened", this);
if (this.generated) {
setTimeout(() => {
notifications.clear(notification);
}, TIMEOUT);
}
}
clicked(notification: any, button?: number) {
// We can only be clicked, when we were opened, at which point the
// notification id is availablfalse
// notification id is available
if (notification !== this.notification) {
return;
}
@ -52,6 +64,7 @@ export class Notification extends EventEmitter {
return;
}
this.emit("clicked", this);
console.log("clicked", notification);
}
async closed(notification: any) {

View File

@ -1,9 +1,9 @@
"use strict";
// License: MIT
import * as DEFAULT_PREFS from "../data/prefs.json";
import DEFAULT_PREFS from "../data/prefs.json";
import { EventEmitter } from "./events";
import {loadOverlay} from "./objectoverlay";
import { loadOverlay } from "./objectoverlay";
import { storage } from "./browser";
const PREFS = Symbol("PREFS");

View File

@ -116,3 +116,9 @@ export const FASTFILTER = new RecentList("fastfilter", [
"*.z??, *.css, *.html"
]);
FASTFILTER.init().catch(console.error);
export const SUBFOLDER = new RecentList("subfolder", [
"",
"downthemall",
]);
SUBFOLDER.init().catch(console.error);

View File

@ -28,7 +28,7 @@ function computeSelection(
items: BaseMatchedItem[],
onlyFast: boolean): ItemDelta[] {
let ws = items.map((item, idx: number) => {
item.idx = idx;
item.idx = item.idx || idx;
const {matched = null} = item;
item.prevMatched = matched;
item.matched = null;
@ -50,7 +50,7 @@ function computeSelection(
return !item.matched;
});
}
return items.filter(item => item.prevMatched !== item.matched). map(item => {
return items.filter(item => item.prevMatched !== item.matched).map(item => {
return {
idx: item.idx,
matched: item.matched
@ -98,6 +98,7 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
type: "popup",
});
const window = await windows.create(windowOptions);
tracker.track(window.id, null);
try {
const port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
@ -186,8 +187,8 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
openPrefs();
});
port.on("openUrls", ({urls}) => {
openUrls(urls);
port.on("openUrls", ({urls, incognito}) => {
openUrls(urls, incognito);
});
try {

View File

@ -21,6 +21,7 @@ export async function single(item: BaseItem | null) {
type: "popup",
});
const window = await windows.create(windowOptions);
tracker.track(window.id, null);
try {
const port: Port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),

View File

@ -2,8 +2,9 @@
// License: MIT
import * as psl from "psl";
import {memoize, identity} from "./memoize";
export {debounce} from "../uikit/lib/util";
import { identity, memoize } from "./memoize";
import { IPReg } from "./ipreg";
export { debounce } from "../uikit/lib/util";
export class Promised {
private promise: Promise<any>;
@ -96,8 +97,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
export const sanitizePath = identity(
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
export class PathInfo {
private baseField: string;
private extField: string;
private pathField: string;
private nameField: string;
private fullField: string;
constructor(base: string, ext: string, path: string) {
this.baseField = base;
this.extField = ext;
this.pathField = path;
this.update();
}
get base() {
return this.baseField;
}
set base(nv) {
this.baseField = sanitizePath(nv);
this.update();
}
get ext() {
return this.extField;
}
set ext(nv) {
this.extField = sanitizePath(nv);
this.update();
}
get name() {
return this.nameField;
}
get path() {
return this.pathField;
}
set path(nv) {
this.pathField = sanitizePath(nv);
this.update();
}
get full() {
return this.fullField;
}
private update() {
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
}
clone() {
return new PathInfo(this.baseField, this.extField, this.pathField);
}
}
// XXX cleanup + test
export const parsePath = memoize(function parsePath(path: string | URL) {
export const parsePath = memoize(function parsePath(
path: string | URL): PathInfo {
if (path instanceof URL) {
path = decodeURIComponent(path.pathname);
}
@ -127,13 +192,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
}
path = pieces.join("/");
return {
path,
name,
base,
ext,
full: path ? `${path}/${name}` : name
};
return new PathInfo(base, ext, path);
});
export class CoalescedUpdate<T> extends Set<T> {
@ -179,7 +238,10 @@ export interface URLd extends URL {
Object.defineProperty(URL.prototype, "domain", {
get() {
try {
return hostToDomain(this.host) || this.host;
const {hostname} = this;
return IPReg.test(hostname) ?
hostname :
hostToDomain(hostname) || hostname;
}
catch (ex) {
console.error(ex);
@ -299,3 +361,20 @@ export function mapFilterInSitu<TRes, T>(
export function randint(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function validateSubFolder(folder: string) {
if (!folder) {
return;
}
folder = folder.replace(/[/\\]+/g, "/");
if (folder.startsWith("/")) {
throw new Error("error.noabsolutepath");
}
if (/^[a-z]:\//i.test(folder)) {
throw new Error("error.noabsolutepath");
}
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
throw new Error("error.nodotsinpath");
}
}

View File

@ -55,13 +55,15 @@ export class WindowStateTracker {
getOptions(options: any) {
const result = Object.assign(options, {
width: this.width,
height: this.height,
state: this.state,
});
if (this.top >= 0) {
result.top = this.top;
result.left = this.left;
if (result.state !== "maximized") {
result.width = this.width;
result.height = this.height;
if (this.top >= 0) {
result.top = this.top;
result.left = this.left;
}
}
return result;
}

View File

@ -2,29 +2,56 @@
// License: MIT
import { windows, tabs, runtime } from "../lib/browser";
import {getManager} from "./manager/man";
import * as DEFAULT_ICONS from "../data/icons.json";
import { getManager } from "./manager/man";
import DEFAULT_ICONS from "../data/icons.json";
import { Prefs } from "./prefs";
import { _ } from "./i18n";
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
const DONATE_LANG_URLS = Object.freeze(new Map([
["de", "https://www.downthemall.org/howto/donate/spenden/"],
]));
const MANAGER_URL = "/windows/manager.html";
const IS_CHROME = navigator && navigator.userAgent.includes("Chrome");
export async function mostRecentBrowser(): Promise<any> {
let window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
filter((w: any) => w.type === "normal").pop();
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
let window;
try {
window = await windows.getCurrent();
if (window.type !== "normal") {
throw new Error("not a normal window");
}
if (incognito && !window.incognito) {
throw new Error("Not incognito");
}
}
catch {
try {
window = await windows.getlastFocused();
if (window.type !== "normal") {
throw new Error("not a normal window");
}
if (incognito && !window.incognito) {
throw new Error("Not incognito");
}
}
catch {
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
filter(
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
pop();
}
}
if (!window) {
window = await windows.create({
url: DONATE_URL,
incognito: !!incognito,
type: "normal",
});
}
return window;
}
export async function openInTab(url: string) {
const window = await mostRecentBrowser();
export async function openInTab(url: string, incognito: boolean) {
const window = await mostRecentBrowser(incognito);
await tabs.create({
active: true,
url,
@ -33,7 +60,7 @@ export async function openInTab(url: string) {
await windows.update(window.id, {focused: true});
}
export async function openInTabOrFocus(url: string) {
export async function openInTabOrFocus(url: string, incognito: boolean) {
const etabs = await tabs.query({
url
});
@ -43,21 +70,22 @@ export async function openInTabOrFocus(url: string) {
await windows.update(tab.windowId, {focused: true});
return;
}
await openInTab(url);
await openInTab(url, incognito);
}
export async function maybeOpenInTab(url: string) {
export async function maybeOpenInTab(url: string, incognito: boolean) {
const etabs = await tabs.query({
url
});
if (etabs.length) {
return;
}
await openInTab(url);
await openInTab(url, incognito);
}
export async function donate() {
await openInTab(DONATE_URL);
const url = DONATE_LANG_URLS.get(_("language_code")) || DONATE_URL;
await openInTab(url, false);
}
export async function openPrefs() {
@ -71,16 +99,38 @@ export async function openManager(focus = true) {
catch (ex) {
console.error(ex.toString(), ex);
}
const url = runtime.getURL(MANAGER_URL);
const openInPopup = await Prefs.get("manager-in-popup");
if (openInPopup) {
const etabs = await tabs.query({
url
});
if (etabs.length) {
if (!focus) {
return;
}
const tab = etabs.pop();
await tabs.update(tab.id, {active: true});
await windows.update(tab.windowId, {focused: true});
return;
}
const windowOptions = {
url,
type: "popup",
};
await windows.create(windowOptions);
return;
}
if (focus) {
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
}
else {
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
}
}
export async function openUrls(urls: string) {
const window = await mostRecentBrowser();
export async function openUrls(urls: string, incognito: boolean) {
const window = await mostRecentBrowser(incognito);
for (const url of urls) {
try {
await tabs.create({
@ -106,32 +156,10 @@ const ICONS = Object.freeze((() => {
return new Map<string, string>(rv);
})());
let iconForPathPlatform: Function;
if (IS_CHROME) {
const FOUR = 128;
const DOUBLE = 64;
iconForPathPlatform = function(icon: string, size: number) {
let scale = "1x";
if (size > FOUR) {
// wishful thinking at this point
scale = "4x";
}
else if (size > DOUBLE) {
scale = "2x";
}
return `chrome://fileicon/${icon}?scale=${scale}`;
};
}
else {
// eslint-disable-next-line no-unused-vars
iconForPathPlatform = function(icon: string, size: number) {
return ICONS.get(icon) || "icon-file-generic";
};
}
export const DEFAULT_ICON_SIZE = 16;
// eslint-disable-next-line no-magic-numbers
export function iconForPath(path: string, size = 16) {
// eslint-disable-next-line no-unused-vars
export function iconForPath(path: string, size = DEFAULT_ICON_SIZE) {
const web = /^https?:\/\//.test(path);
let file = path.split(/[\\/]/).pop();
if (file) {
@ -152,7 +180,7 @@ export function iconForPath(path: string, size = 16) {
file = "file";
}
}
return iconForPathPlatform(file, size);
return ICONS.get(file) || "icon-file-generic";
}
/**

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "DownThemAll!",
"version": "4.0.3",
"version": "4.1.2",
"description": "__MSG_extensionDescription__",
"homepage_url": "https://downthemall.org/",
@ -9,6 +9,8 @@
"default_locale": "en",
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: http: https: 'self'; default-src 'self'",
"icons": {
"16": "style/icon16.png",
"32": "style/icon32.png",
@ -22,14 +24,18 @@
"permissions": [
"<all_urls>",
"contextMenus",
"menus",
"downloads",
"downloads.open",
"downloads.shelf",
"history",
"menus",
"notifications",
"sessions",
"storage",
"tabs",
"webNavigation"
"webNavigation",
"webRequest",
"webRequestBlocking"
],
"background": {

View File

@ -22,18 +22,20 @@
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"chai": "^4.1.2",
"eslint": "^6.1.0",
"eslint": "^6.2.2",
"mocha": "^6.2.0",
"ts-loader": "^6.0.4",
"ts-node": "^8.3.0",
"typescript": "^3.5.3",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.6",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"xregexp": "^4.2.4"
},
"dependencies": {
"@types/psl": "^1.1.0",
"@types/whatwg-mimetype": "^2.1.0",
"psl": "^1.3.0",
"webextension-polyfill": "^0.4.0"
"webextension-polyfill": "^0.4.0",
"whatwg-mimetype": "^2.3.0"
}
}

View File

@ -77,6 +77,8 @@ function urlToUsable(e: any, u: string) {
}
class Gatherer {
private: boolean;
textLinks: boolean;
selectionOnly: boolean;
@ -88,6 +90,7 @@ class Gatherer {
transferable: string[];
constructor(options: any) {
this.private = !!options.private;
this.textLinks = options.textLinks;
this.selectionOnly = options.selectionOnly;
this.selection = options.selectionOnly ? getSelection() : null;
@ -255,6 +258,7 @@ class Gatherer {
return {
url: url.href,
title,
private: this.private
};
}
catch (ex) {
@ -295,7 +299,7 @@ class Gatherer {
function gather(msg: any, sender: any, callback: Function) {
try {
if (!msg || msg.type !== "DTA:gather" || !callback) {
return;
return Promise.resolve(null);
}
const gatherer = new Gatherer(msg);
const result = {
@ -313,10 +317,11 @@ function gather(msg: any, sender: any, callback: Function) {
),
};
urlToUsable(result, result.baseURL);
callback(result);
return Promise.resolve(result);
}
catch (ex) {
console.error(ex.toString(), ex.stack, ex);
return Promise.resolve(null);
}
}

BIN
sounds/done.wav Normal file

Binary file not shown.

BIN
sounds/error.wav Normal file

Binary file not shown.

View File

@ -10,6 +10,7 @@
--add-color: navy;
--queue-color: gray;
--pause-color: #ffa318;
--retry-color: rgb(0, 112, 204);
--error-color: rgb(160, 13, 42);
--running-color: #aae061;
--finishing-color: #57cc12;
@ -18,12 +19,17 @@
--folder-color: rgb(214, 165, 4);
--maskbutton-color: rgb(236, 185, 16);
--missing-color: rgb(0, 82, 204);
--open-color: rgba(236, 185, 16, 0.8);
}
html[data-platform="mac"] {
--folder-color: rgb(4, 102, 214);
}
html, body {
font-size: 10pt !important;
}
@font-face {
font-family: 'downthemall';
src: url('downthemall.woff2?75791791') format('woff2');
@ -88,6 +94,7 @@ html[data-platform="mac"] {
.icon-file-video:before { content: '\e825'; } /* '' */
.icon-file-generic:before { content: '\e826'; } /* '' */
.icon-question-dark:before { content: '\e827'; } /* '' */
.icon-forward:before { content: '\e828'; } /* '' */
.icon-filter:before { content: '\f0b0'; } /* '' */
.icon-donate:before { content: '\f0d6'; } /* '' */
.icon-file-doc:before { content: '\f0f6'; } /* '' */
@ -100,7 +107,8 @@ html[data-platform="mac"] {
.icon-file-image:before { content: '\f1c5'; } /* '' */
.icon-file-archive:before { content: '\f1c6'; } /* '' */
.icon-file-audio:before { content: '\f1c7'; } /* '' */
.icon-toggle:before { content: '\f205'; } /* '' */
.icon-toggle-off:before { content: '\f204'; } /* '' */
.icon-toggle-on:before { content: '\f205'; } /* '' */
.icon-server:before { content: '\f233'; } /* '' */
.icon-question-light:before { content: '\f29c'; } /* '' */

BIN
style/done.opus Normal file

Binary file not shown.

BIN
style/downthemall.woff2 Normal file → Executable file

Binary file not shown.

BIN
style/error.opus Normal file

Binary file not shown.

View File

@ -108,11 +108,11 @@ body > * {
}
#colURL {
width: 38%;
width: 42%;
}
#colPercent {
width: 3em;
width: 4em;
min-width: 3em;
}
@ -121,11 +121,11 @@ body > * {
}
#colSize {
width: 15em;
width: 14em;
}
#colSpeed {
width: 6em;
width: 7em;
}
#colDomain,
@ -154,6 +154,14 @@ body > * {
height: 26px;
}
.virtualtable-row.opening {
background: var(--open-color) !important;
}
.virtualtable-progress-container {
border-radius: 2px;
}
.virtualtable-progress-bar {
height: 14px;
}
@ -194,6 +202,23 @@ body > * {
);
}
.retrying .virtualtable-column-2 .virtualtable-icon {
color: var(--retry-color);
}
.retrying .virtualtable-column-2 .virtualtable-progress-bar {
background: var(--retry-color);
}
.retrying .virtualtable-column-2 .virtualtable-progress-undetermined {
background: repeating-linear-gradient(
45deg,
var(--retry-color),
var(--retry-color) 6px,
transparent 6px,
transparent 12px
);
}
.missing .virtualtable-column-2 .virtualtable-icon,
.canceled .virtualtable-column-2 .virtualtable-icon {
color: var(--error-color);
@ -262,6 +287,7 @@ body > * {
}
.virtualtable-column-6,
.virtualtable-column-4,
.virtualtable-column-3 {
text-align: right;
}
@ -318,6 +344,7 @@ body > * {
height: 16px;
-moz-appearance: none;
border: 0;
outline: 0;
background: transparent;
width: calc(100% - 28px);
}
@ -430,6 +457,8 @@ body > * {
justify-items: stretch;
border-radius: 4px;
box-shadow: 2px 2px 6px black;
-webkit-user-select: none;
user-select: none;
}
#tooltip-infos {
@ -442,8 +471,13 @@ body > * {
}
#tooltip-icon {
font-size: 48px;
line-height: 48px;
height: 64px;
width: 64px;
background-size: 64px 64px;
background-repeat: no-repeat;
background-position: center center;
font-size: 64px;
line-height: 64px;
padding: 6px;
text-align: center;
grid-row: 1/-1;
@ -495,4 +529,24 @@ body > * {
height: 100%;
width: 100%;
background: var(--done-color);
}
#tooltip-eta.single {
font-weight: bold;
grid-column-end: span 2;
}
.deletefiles-list {
padding-left: 1ex;
padding-right: 1.5ex;
border: 1px solid lightgray;
border-radius: 6px;
background-color: rgba(128,128,128,0.1);
max-height: 8em;
overflow-y: auto;
}
.deletefiles-list > li {
list-style-type: none;
padding: 0;
margin: 0;
}

View File

@ -138,4 +138,11 @@ legend {
border-radius: 6px;
background: rgba(128, 128, 128, 0.05);
box-shadow: 1px 1px 6px lightgray;
}
#network-general {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: 1em;
grid-row-gap: 1ex;
}

View File

@ -232,3 +232,7 @@ body > * {
#maskButton {
justify-self: flex-start;
}
#btnDownload {
font-weight: bold;
}

View File

@ -49,7 +49,7 @@ p.example {
}
#options > * {
margin: 0;
margin: 2px;
}
#options > input {
@ -63,6 +63,7 @@ p.example {
align-items: center;
}
#options > #subfolderOptions,
#options > #maskOptions {
display: grid;
grid-template-columns: 2fr auto auto;
@ -81,3 +82,7 @@ h3 {
font-weight: normal;
font-style: italic;
}
#btnDownload {
font-weight: bold;
}

View File

@ -0,0 +1,289 @@
/* eslint-disable max-len */
/* eslint-env node */
"use strict";
// License: MPL-2
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { CDHeaderParser } = require("../lib/cdheaderparser");
const parser = new CDHeaderParser();
function check(header, expected) {
expect(parser.parse(header)).to.equal(expected);
}
function nocheck(header, expected) {
expect(parser.parse(header)).not.to.equal(expected);
}
describe("CDHeaderParser", function() {
it("parse wget", function() {
// From wget, test_parse_content_disposition
// http://git.savannah.gnu.org/cgit/wget.git/tree/src/http.c?id=8551ceccfedb4390fbfa82c12f0ff714dab1ac76#n5325
check("filename=\"file.ext\"", "file.ext");
check("attachment; filename=\"file.ext\"", "file.ext");
check("attachment; filename=\"file.ext\"; dummy", "file.ext");
check("attachment", ""); // wget uses NULL, we use "".
check("attachement; filename*=UTF-8'en-US'hello.txt", "hello.txt");
check("attachement; filename*0=\"hello\"; filename*1=\"world.txt\"",
"helloworld.txt");
check("attachment; filename=\"A.ext\"; filename*=\"B.ext\"", "B.ext");
check("attachment; filename*=\"A.ext\"; filename*0=\"B\"; filename*1=\"B.ext\"",
"A.ext");
// This test is faulty - https://savannah.gnu.org/bugs/index.php?52531
//check("filename**0=\"A\"; filename**1=\"A.ext\"; filename*0=\"B\";filename*1=\"B\"", "AA.ext");
});
it("parse Firefox", function() {
// From Firefox
// https://searchfox.org/mozilla-central/rev/45a3df4e6b8f653b0103d18d97c34dd666706358/netwerk/test/unit/test_MIME_params.js
// Changed as follows:
// - Replace error codes with empty string (we never throw).
const BS = "\\";
const DQUOTE = "\"";
// No filename parameter: return nothing
check("attachment;", "");
// basic
check("attachment; filename=basic", "basic");
// extended
check("attachment; filename*=UTF-8''extended", "extended");
// prefer extended to basic (bug 588781)
check("attachment; filename=basic; filename*=UTF-8''extended", "extended");
// prefer extended to basic (bug 588781)
check("attachment; filename*=UTF-8''extended; filename=basic", "extended");
// use first basic value (invalid; error recovery)
check("attachment; filename=first; filename=wrong", "first");
// old school bad HTTP servers: missing 'attachment' or 'inline'
// (invalid; error recovery)
check("filename=old", "old");
check("attachment; filename*=UTF-8''extended", "extended");
// continuations not part of RFC 5987 (bug 610054)
check("attachment; filename*0=foo; filename*1=bar", "foobar");
// Return first continuation (invalid; error recovery)
check("attachment; filename*0=first; filename*0=wrong; filename=basic", "first");
// Only use correctly ordered continuations (invalid; error recovery)
check("attachment; filename*0=first; filename*1=second; filename*0=wrong", "firstsecond");
// prefer continuation to basic (unless RFC 5987)
check("attachment; filename=basic; filename*0=foo; filename*1=bar", "foobar");
// Prefer extended to basic and/or (broken or not) continuation
// (invalid; error recovery)
check("attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended", "extended");
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
// (invalid; error recovery)
check("attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar", "extended");
// Gaps should result in returning only value until gap hit
// (invalid; error recovery)
check("attachment; filename*0=foo; filename*2=bar", "foo");
// Don't allow leading 0's (*01) (invalid; error recovery)
check("attachment; filename*0=foo; filename*01=bar", "foo");
// continuations should prevail over non-extended (unless RFC 5987)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*2*=%20extended",
"multiline extended");
// Gaps should result in returning only value until gap hit
// (invalid; error recovery)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*3*=%20extended",
"multiline");
// First series, only please, and don't slurp up higher elements (*2 in this
// case) from later series into earlier one (invalid; error recovery)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*0*=UTF-8''wrong;\r\n" +
" filename*1=bad;\r\n" +
" filename*2=evil",
"multiline");
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
// (invalid; error recovery)
check("attachment; filename=basic; filename*0=UTF-8''multi\r\n;" +
" filename*=UTF-8''extended;\r\n" +
" filename*1=line;\r\n" +
" filename*2*=%20extended",
"extended");
// sneaky: if unescaped, make sure we leave UTF-8'' in value
check("attachment; filename*0=UTF-8''unescaped;\r\n" +
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
"UTF-8''unescaped so includes UTF-8'' in value");
// sneaky: if unescaped, make sure we leave UTF-8'' in value
check("attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" +
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
"UTF-8''unescaped so includes UTF-8'' in value");
// Prefer basic over invalid continuation
// (invalid; error recovery)
check("attachment; filename=basic; filename*1=multi;\r\n" +
" filename*2=line;\r\n" +
" filename*3*=%20extended",
"basic");
// support digits over 10
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n",
"0123456789abcdef");
// support digits over 10 (detect gaps)
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*14=e\r\n",
"0123456789abc");
// return nothing: invalid
// (invalid; error recovery)
check("attachment; filename*1=multi;\r\n" +
" filename*2=line;\r\n" +
" filename*3*=%20extended",
"");
// Bug 272541: Empty disposition type treated as "attachment"
// sanity check
check("attachment; filename=foo.html", "foo.html");
// the actual bug
check("; filename=foo.html", "foo.html");
// regression check, but see bug 671204
check("filename=foo.html", "foo.html");
// Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order
// check ordering
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n",
"0123456789abcdef");
// check non-digits in sequence numbers
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1a=1\r\n",
"0");
// check duplicate sequence numbers
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*0=bad; filename*1=1;\r\n",
"0");
// check overflow
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*11111111111111111111111111111111111111111111111111111111111=1",
"0");
// check underflow
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*-1=1",
"0");
// check mixed token/quoted-string
check("attachment; filename=basic; filename*0=\"0\";\r\n" +
" filename*1=1;\r\n" +
" filename*2*=%32",
"012");
// check empty sequence number
check("attachment; filename=basic; filename**=UTF-8''0\r\n", "basic");
// Bug 419157: ensure that a MIME parameter with no charset information
// fallbacks to Latin-1
check("attachment;filename=IT839\x04\xB5(m8)2.pdf;", "IT839\u0004\u00b5(m8)2.pdf");
// Bug 588389: unescaping backslashes in quoted string parameters
// '\"', should be parsed as '"'
check(`attachment; filename=${DQUOTE}${BS + DQUOTE}${DQUOTE}`, DQUOTE);
// 'a\"b', should be parsed as 'a"b'
check(`attachment; filename=${DQUOTE}a${BS + DQUOTE}b${DQUOTE}`, `a${DQUOTE}b`);
// '\x', should be parsed as 'x'
check(`attachment; filename=${DQUOTE}${BS}x${DQUOTE}`, "x");
// test empty param (quoted-string)
check(`attachment; filename=${DQUOTE}${DQUOTE}`, "");
// test empty param
check("attachment; filename=", "");
// Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP)
check("attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=", "foo-\u00e4.html");
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"", "foo-\u00e4.html");
// format sent by GMail as of 2012-07-23 (5987 overrides 2047)
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987", "5987");
// Bug 651185: double quotes around 2231/5987 encoded param
// Change reverted to backwards compat issues with various web services,
// such as OWA (Bug 703015), plus similar problems in Thunderbird. If this
// is tried again in the future, email probably needs to be special-cased.
// sanity check
check("attachment; filename*=utf-8''%41", "A");
// the actual bug
check(`attachment; filename*=${DQUOTE}utf-8''%41${DQUOTE}`, "A");
// Bug 670333: Content-Disposition parser does not require presence of "="
// in params
// sanity check
check("attachment; filename*=UTF-8''foo-%41.html", "foo-A.html");
// the actual bug
check("attachment; filename *=UTF-8''foo-%41.html", "");
// the actual bug, without 2231/5987 encoding
check("attachment; filename X", "");
// sanity check with WS on both sides
check("attachment; filename = foo-A.html", "foo-A.html");
// Bug 685192: in RFC2231/5987 encoding, a missing charset field should be
// treated as error
// the actual bug
check("attachment; filename*=''foo", "foo");
// sanity check
check("attachment; filename*=a''foo", "foo");
// Bug 692574: RFC2231/5987 decoding should not tolerate missing single
// quotes
// one missing
check("attachment; filename*=UTF-8'foo-%41.html", "foo-A.html");
// both missing
check("attachment; filename*=foo-%41.html", "foo-A.html");
// make sure fallback works
check("attachment; filename*=UTF-8'foo-%41.html; filename=bar.html", "foo-A.html");
// Bug 693806: RFC2231/5987 encoding: charset information should be treated
// as authoritative
// UTF-8 labeled ISO-8859-1
check("attachment; filename*=ISO-8859-1''%c3%a4", "\u00c3\u00a4");
// UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1
// accepts x82, understands it as Win1252, maps it to Unicode \u20a1
check("attachment; filename*=ISO-8859-1''%e2%82%ac", "\u00e2\u201a\u00ac");
// defective UTF-8
nocheck("attachment; filename*=UTF-8''A%e4B", "");
// defective UTF-8, with fallback
nocheck("attachment; filename*=UTF-8''A%e4B; filename=fallback", "fallback");
// defective UTF-8 (continuations), with fallback
nocheck("attachment; filename*0*=UTF-8''A%e4B; filename=fallback", "fallback");
// check that charsets aren't mixed up
check("attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4", "currency-sign=\u00a4");
// same as above, except reversed
check("attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4", "currency-sign=\u00a4");
// Bug 704989: add workaround for broken Outlook Web App (OWA)
// attachment handling
check("attachment; filename*=\"a%20b\"", "a b");
// Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal
check("attachment; filename=\"", "");
// We used to read past string if last param w/o = and ;
// Note: was only detected on windows PGO builds
check("attachment; filename=foo; trouble", "foo");
// Same, followed by space, hits another case
check("attachment; filename=foo; trouble ", "foo");
check("attachment", "");
// Bug 730574: quoted-string in RFC2231-continuations not handled
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\r.html\"", "foobar.html");
// unmatched escape char
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\", "fooba\\");
// Bug 732369: Content-Disposition parser does not require presence of ";" between params
// optimally, this would not even return the disposition type "attachment"
check("attachment; extension=bla filename=foo", "");
check("attachment; filename=foo extension=bla", "foo");
check("attachment filename=foo", "");
// Bug 777687: handling of broken %escapes
nocheck("attachment; filename*=UTF-8''f%oo; filename=bar", "bar");
nocheck("attachment; filename*=UTF-8''foo%; filename=bar", "bar");
// Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer
check("attachment; filename=\"\\b\\a\\", "ba\\");
});
it("parse extra", function() {
// Extra tests, not covered by above tests.
check("inline; FILENAME=file.txt", "file.txt");
check("INLINE; FILENAME= \"an example.html\"", "an example.html"); // RFC 6266, section 5.
check("inline; filename= \"tl;dr.txt\"", "tl;dr.txt");
check("INLINE; FILENAME*= \"an example.html\"", "an example.html");
check("inline; filename*= \"tl;dr.txt\"", "tl;dr.txt");
check("inline; filename*0=\"tl;dr and \"; filename*1=more.txt", "tl;dr and more.txt");
});
it("parse issue 26", function() {
// https://github.com/Rob--W/open-in-browser/issues/26
check("attachment; filename=\xe5\x9c\x8b.pdf", "\u570b.pdf");
});
it("parse issue 35", function() {
// https://github.com/Rob--W/open-in-browser/issues/35
check("attachment; filename=okre\x9clenia.rtf", "okreœlenia.rtf");
});
});

30
tests/test_mime.js Normal file
View File

@ -0,0 +1,30 @@
"use strict";
// License: CC0 1.0
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {MimeDB} = require("../lib/mime");
describe("MIME", function() {
it("general", function() {
expect(MimeDB.getMime("image/jpeg").major).to.equal("image");
expect(MimeDB.getMime("image/jpeg").minor).to.equal("jpeg");
expect(MimeDB.getMime("iMage/jPeg").major).to.equal("image");
expect(MimeDB.getMime("imAge/jpEg").minor).to.equal("jpeg");
});
it("exts", function() {
expect(MimeDB.getMime("image/jpeg").primary).to.equal("jpg");
expect(MimeDB.getMime("image/jpeg").primary).to.equal(
MimeDB.getPrimary("image/jpeg"));
expect(MimeDB.getMime("iMage/jPeg").primary).to.equal("jpg");
expect(MimeDB.getMime("imAge/jpEg").primary).to.equal(
MimeDB.getPrimary("image/jpeg"));
expect(Array.from(MimeDB.getMime("imAge/jpEg").extensions)).to.deep.equal(
["jpg", "jpeg", "jpe", "jfif"]);
});
it("application/octet-stream should not yield results", function() {
expect(MimeDB.getPrimary("application/octet-stream")).to.equal("");
expect(MimeDB.getMime("application/octet-Stream")).to.be.undefined;
});
});

View File

@ -19,7 +19,7 @@ const OPTS = {
state: DownloadState.QUEUED,
batch: 42,
idx: 23,
mask: "*name*.*ext",
mask: "*name*.*ext*",
description: "desc / ript.ion .",
title: " *** TITLE *** ",
};
@ -57,6 +57,49 @@ describe("Renamer", function() {
expect(dest.path).to.equal("");
});
it("*name*.*ext* (mime override)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "image/jpeg"
}));
expect(dest.full).to.equal("filenäme 042.jpg");
expect(dest.name).to.equal("filenäme 042.jpg");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("jpg");
expect(dest.path).to.equal("");
});
it("*name*.*ext* (mime no override)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "image/jpeg",
url: "https://www.example.co.uk/filen%C3%A4me.JPe",
usable: "https://www.example.co.uk/filenäme.JPe",
}));
expect(dest.full).to.equal("filenäme 042.JPe");
expect(dest.name).to.equal("filenäme 042.JPe");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("JPe");
expect(dest.path).to.equal("");
});
it("*name*.*ext* (mime override; missing ext)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "application/json",
url: "https://www.example.co.uk/filen%C3%A4me",
usable: "https://www.example.co.uk/filenäme",
}));
expect(dest.full).to.equal("filenäme 042.json");
expect(dest.name).to.equal("filenäme 042.json");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("json");
expect(dest.path).to.equal("");
});
it("*text*", function() {
const dest = makeOne("*text*");
expect(dest.full).to.equal("desc/ript.ion");

43
tests/test_urld.js Normal file
View File

@ -0,0 +1,43 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
"use strict";
// License: CC0 1.0
require("../lib/util");
describe("URLd", function() {
it("basic domain", function() {
let u = new URL("https://www.google.de");
expect(u.domain).to.equal("google.de");
u = new URL("https://www.google.de:8443");
expect(u.domain).to.equal("google.de");
});
it("plain basic domain", function() {
const u = new URL("https://google.de");
expect(u.domain).to.equal("google.de");
});
it("special domain", function() {
let u = new URL("https://www.google.co.uk");
expect(u.domain).to.equal("google.co.uk");
u = new URL("https://google.co.uk");
expect(u.domain).to.equal("google.co.uk");
u = new URL("https://www.google.co.uk:8443");
expect(u.domain).to.equal("google.co.uk");
});
it("ipv4", function() {
let u = new URL("https://127.0.0.1:8443");
expect(u.domain).to.equal("127.0.0.1");
u = new URL("https://0.0.0.0:8443");
expect(u.domain).to.equal("0.0.0.0");
});
it("ipv6", function() {
let u = new URL("https://[::1]:8443");
expect(u.domain).to.equal("[::1]");
u = new URL("https://[2a00:1450:4005:800::2003]:8443");
expect(u.domain).to.equal("[2a00:1450:4005:800::2003]");
});
});

View File

@ -12,6 +12,8 @@
"noImplicitReturns": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"importHelpers": true,
"sourceMap": true
}
}

View File

@ -33,7 +33,7 @@
.modal-footer {
display: flex;
flex-wrap: nowrap;
justify-content: right;
justify-content: flex-end;
background: rgba(30, 30, 30, 0.2);
margin-top: 2em;
border-top: 1px solid rgba(30, 30, 30, 0.5);

View File

@ -10,7 +10,7 @@ const MENU_OPEN_BOUNCE = 500;
let ids = 0;
const Keys = new Map([
export const Keys = new Map([
["ACCEL", IS_MAC ? "⌘" : "Ctrl"],
["CTRL", "Ctrl"],
["ALT", IS_MAC ? "⌥" : "Alt"],

View File

@ -15,16 +15,20 @@ export function addClass(elem: HTMLElement, ...cls: string[]) {
interface Timer {
args: any[];
id: number;
}
export function debounce(fn: Function, to: number) {
export function debounce(fn: Function, to: number, reset?: boolean) {
let timer: Timer | null;
return function(...args: any[]) {
if (timer) {
timer.args = args;
return;
if (!reset) {
timer.args = args;
return;
}
window.clearTimeout(timer.id);
}
setTimeout(function() {
const id = window.setTimeout(function() {
if (!timer) {
return;
}
@ -37,7 +41,7 @@ export function debounce(fn: Function, to: number) {
console.error(ex.toString(), ex);
}
}, to);
timer = {args};
timer = {args, id};
};
}

23
util/additional.types Normal file
View File

@ -0,0 +1,23 @@
types {
application/x-x509-ca-cert pem crt der;
application/javascript js jsx;
audio/x-matroska mka;
image/bmp bmp;
image/heic heic heif;
image/heic heic heif;
image/heif-sequence heic heif;
image/heif-sequence heic heif;
image/jpeg jpg jpeg jpe jfif;
image/webp webp;
text/html html htm shtml php;
text/javascript js jsx;
video/mpeg mpg mpe mpeg mpg;
video/opus opus;
video/x-matroska mkv mk3d mks;
video/quicktime mov qt moov;
application/x-compressed gz;
application/x-gzip gz gzip;
application/x-bzip2 bz2;
application/x-tar tar;
application/x-xz xz;
}

View File

@ -70,7 +70,7 @@ def main():
if modified:
try:
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
json.dump(data, outp, sort_keys=True, indent=2)
json.dump(data, outp, sort_keys=True, indent=2, ensure_ascii=False)
os.rename("messages.json.tmp", "_locales/en/messages.json")
finally:
try:

View File

@ -26,7 +26,8 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
LICENSED = set((".css", ".html", ".js", "*.ts"))
IGNORED = set((".DS_Store", "Thumbs.db"))
PERM_IGNORED_FX = set(("downloads.shelf",))
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest", "webRequestBlocking"))
PERM_IGNORED_CHROME = set(("menus", "sessions"))
SCRIPTS = [
"yarn build:regexps",
@ -90,8 +91,6 @@ def build_firefox(args):
else:
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_FX]
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-fx.zip"
if not out.parent.exists():
@ -101,6 +100,33 @@ def build_firefox(args):
print("Output", out)
build(out, json.dumps(infos, indent=2).encode("utf-8"))
def build_chrome(args):
now = datetime.now().strftime("%Y%m%d%H%M%S")
with open("manifest.json") as manip:
infos = json.load(manip, object_pairs_hook=OrderedDict)
version = infos.get("version")
if args.mode == "nightly":
version = infos["version"] = f"{version}.{now}"
version = infos.get("version")
del infos["browser_specific_settings"]
if args.mode != "release":
infos["version_name"] = f"{version}-{args.mode}"
infos["short_name"] = infos.get("name")
infos["name"] = f"{infos.get('name')} {args.mode}"
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_CHROME]
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-crx.zip"
if not out.parent.exists():
out.parent.mkdir()
if out.exists():
out.unlink()
print("Output", out)
build(out, json.dumps(infos, indent=2).encode("utf-8"))
def main():
from argparse import ArgumentParser
args = ArgumentParser()
@ -114,6 +140,7 @@ def main():
else:
run([script], shell=True)
build_firefox(args)
build_chrome(args)
print("DONE.")
if __name__ == "__main__":

25
util/i18ntochrome.py Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import json
import re
from collections import OrderedDict
from pathlib import Path
re_valid = re.compile("[^A-Za-z0-9_]")
for file in Path("_locales/").glob("**/*.json"):
with file.open("r") as filep:
messages = json.load(filep, object_pairs_hook=OrderedDict)
for x in list(messages):
prev = x
while True:
y = re_valid.sub("_", x)
if prev == y:
break
prev = y
if x == y:
continue
messages[y] = messages[x]
del messages[x]
with file.open("w", encoding="utf-8") as filep:
json.dump(messages, filep, ensure_ascii=False, indent=2)

16
util/makelocalelist.py Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
import json
from pathlib import Path
langs = sorted(Path("_locales").glob("**/messages.json"), key=lambda p: p.parent.name.casefold())
all = {}
for m in langs:
loc = m.parent.name
with m.open("r") as mp:
lang = json.load(mp).get("language").get("message")
if not lang:
raise Exception(f"{m}: no language")
lang = f"{lang} [{loc}]"
all[loc] = lang
with open("_locales/all.json", "wb") as op:
op.write(json.dumps(all, indent=2, ensure_ascii=False).encode("utf-8"))

98
util/mime.types Normal file
View File

@ -0,0 +1,98 @@
https://github.com/nginx/nginx/raw/master/conf/mime.types
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

76
util/seed_mime.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
import json
import re
import sys
from collections import OrderedDict
def unique(seq):
return list(OrderedDict([i, None] for i in seq if i))
def generate(major, minor, exts):
exts = exts[:]
yield f"{major}/{minor}", exts
if (minor.startswith("x-")):
yield f"{major}/{minor[2:]}", exts
else:
yield f"{major}/x-{minor}", exts
for ext in exts:
yield f"{major}/{ext}", exts
yield f"{major}/x-{ext}", exts
def make(text, final):
lines = "".join([
line.strip()
for line in re.search(r"\{(.*)\}", text, re.S).group(1).split("\n")
if line.strip() and not line.strip().startswith("#")
]).split(";")
additional = []
for line in lines:
if not line:
continue
m = re.match(r"([a-z1-9]+)/([^\s]+)\s+(.+?)$", line)
if not m:
continue
[major, minor, exts] = m.groups()
exts = unique(e.lower().strip() for e in exts.split(" ") if e.strip())
mime = f"{major}/{minor}"
if mime == "application/octet-stream":
continue
if mime in final:
final[mime] += exts
continue
final[mime] = exts
additional += (major, minor, exts),
for [major, minor, exts] in additional:
for [mime, exts] in generate(major, minor, exts):
if mime in final:
continue
final[mime] = exts
final = OrderedDict()
for file in sys.argv[1:]:
with open(file, "r") as fp:
make(fp.read(), final)
multi = dict()
for [mime, exts] in list(final.items()):
exts = unique(exts)
prim = exts[0]
final[mime] = prim
if len(exts) == 1:
continue
exts = exts[1:]
if len(exts) == 1:
multi[prim] = exts[0]
else:
multi[prim] = exts
final = OrderedDict(sorted(final.items()))
multi = OrderedDict(sorted(multi.items()))
print(json.dumps(dict(e=multi, m=final), indent=2))
print("generated", len(final), "mimes", "with", len(multi), "multis", file=sys.stderr)

View File

@ -32,7 +32,7 @@ module.exports = {
if (request === "crypto") {
return callback(null, "crypto");
}
if (request.includes("_locales")) {
if (/_locales.*messages\.json/.test(request)) {
return callback(null, "null");
}
return callback();
@ -42,6 +42,11 @@ module.exports = {
filename: "[name].js"
},
devtool: "source-map",
stats: {
hash: true,
timings: true,
maxModules: 2,
},
watchOptions: {
ignored: /node_modules|bundles/
},

22
windows/contextmenu.ts Normal file
View File

@ -0,0 +1,22 @@
"use strict";
// License: MIT
export * from "../uikit/lib/contextmenu";
import { Keys } from "../uikit/lib/contextmenu";
import { IS_MAC } from "../uikit/lib/util";
import { locale, _ } from "../lib/i18n";
locale.then(() => {
Keys.clear();
[
["ACCEL", IS_MAC ? "⌘" : _("key-ctrl")],
["CTRL", _("key-ctrl")],
["ALT", IS_MAC ? "⌥" : _("key-alt")],
["DELETE", _("key-delete")],
["PAGEUP", _("key-pageup")],
["PAGEDOWN", _("key-pagedown")],
["HOME", _("key-home")],
["END", _("key-end")],
["SHIFT", "⇧"],
].forEach(([k, v]) => Keys.set(k, v));
});

View File

@ -22,6 +22,9 @@ export class Dropdown extends EventEmitter {
this.container = document.createElement("div");
this.container.classList.add("dropdown");
if (input.id) {
this.container.id = `${input.id}-dropdown`;
}
input = input.parentElement.replaceChild(this.container, input);
this.input = input as HTMLInputElement;

View File

@ -19,7 +19,7 @@ export class Icons extends Map {
}
let cls = super.get(url);
if (!cls) {
cls = `icon-${++this.running}`;
cls = `iconcache-${++this.running}`;
const rule = `.${cls} { background-image: url(${url}); }`;
this.sheet.insertRule(rule);
super.set(url, cls);

View File

@ -3,7 +3,7 @@
import {EventEmitter} from "../lib/events";
// eslint-disable-next-line no-unused-vars
import { ContextMenu } from "../uikit/lib/contextmenu";
import { ContextMenu } from "./contextmenu";
import { runtime } from "../lib/browser";
export const Keys = new class extends EventEmitter {

View File

@ -75,6 +75,7 @@
<ul id="table-context" class="table-context">
<li id="ctx-open-file" data-key="Enter" data-i18n="open-file" data-icon="icon-file">Open File</li>
<li id="ctx-open-directory" data-key="ACCEL-Enter" data-i18n="open-directory" data-icon="icon-folder">Open Directory</li>
<li id="ctx-delete-files" data-key="ACCEL-Delete" data-i18n="deletefiles" data-icon="icon-delete"></li>
<li>-</li>
<li id="ctx-resume" data-key="ACCEL-KeyR" data-i18n="resume-download" data-icon="icon-go">Resume</li>
<li id="ctx-pause" data-key="ACCEL-KeyP" data-i18n="pause-download" data-icon="icon-pause">Pause</li>
@ -109,6 +110,15 @@
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
<li>-</li>
<li id="ctx-import" data-icon="icon-import" data-i18n="import"></li>
<li id="ctx-export" data-icon="icon-download" data-i18n="export">
<ul class="table-context">
<li id="ctx-export-text" data-i18n="export-text"></li>
<li id="ctx-export-aria2" data-i18n="export-aria2"></li>
<li id="ctx-export-metalink" data-i18n="export-metalink"></li>
</ul>
</li>
<li>-</li>
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
@ -125,11 +135,17 @@
</p>
</template>
<template id="deletefiles-template">
<h1 class="deletefiles-title" data-i18n="deletefiles_title"></h1>
<p class="deletefiles-text" data-i18n="deletefiles_text"></p>
<ul class="deletefiles-list"></ul>
</template>
<template id="menufilter-template">
<ul>
<li id="ctx-menufilter-seperator">-</li>
<li id="ctx-menufilter-invert" data-autoHide="false">Invert</li>
<li id="ctx-menufilter-clear" data-autoHide="false">Clear</li>
<li id="ctx-menufilter-invert" data-auto-hide="false">Invert</li>
<li id="ctx-menufilter-clear" data-auto-hide="false">Clear</li>
<li>-</li>
<li id="ctx-menufilter-sort-ascending" data-icon="icon-sort-asc">Sort ascending</li>
<li id="ctx-menufilter-sort-descending" data-icon="icon-sort-desc">Sort descending</li>

View File

@ -8,6 +8,7 @@ import PORT from "./manager/port";
import { runtime } from "../lib/browser";
import { Promised } from "../lib/util";
import { PromiseSerializer } from "../lib/pserializer";
import { Keys } from "./keys";
const $ = document.querySelector.bind(document);
@ -44,7 +45,7 @@ addEventListener("DOMContentLoaded", function dom() {
const fullyloaded = Promise.all([LOADED, platformed, tabled, localized]);
fullyloaded.then(async () => {
const nag = await Prefs.get("nagging", 0);
const nagnext = await Prefs.get("nagging-next", 6);
const nagnext = await Prefs.get("nagging-next", 7);
const next = Math.ceil(Math.log2(Math.max(1, nag)));
const el = $("#nagging");
const remove = () => {
@ -120,6 +121,11 @@ addEventListener("DOMContentLoaded", function dom() {
statusNetwork.setAttribute("title", _("statusNetwork-inactive.title"));
}
});
Keys.on("ACCEL-KeyF", () => {
$("#filter").focus();
return true;
});
});
addEventListener("contextmenu", event => {

View File

@ -10,7 +10,7 @@ import {
MenuItemBase,
// eslint-disable-next-line no-unused-vars
MenuPosition,
} from "../../uikit/lib/contextmenu";
} from "../contextmenu";
import {EventEmitter} from "../../lib/events";
// eslint-disable-next-line no-unused-vars
import {filters, Matcher, Filter} from "../../lib/filters";
@ -94,8 +94,10 @@ export class TextFilter extends ItemFilter {
}
allow(item: DownloadItem) {
return this.expr.test(
[item.usable, item.description, item.finalName].join(" "));
const {expr} = this;
return expr.test(item.currentName) ||
expr.test(item.usable) ||
expr.test(item.description);
}
}
@ -340,14 +342,19 @@ export class UrlMenuFilter extends MenuFilter {
async populate() {
const filts = await filters();
for (const i of filts.all.filter(e => e.id !== "deffilter-all")) {
this.addItem(i.label, this.toggleRegularFilter.bind(this, i));
this.addItem(
i.label, this.toggleRegularFilter.bind(this, i), this.filters.has(i));
}
this.addItem("-");
sort(
const domains = sort(
Array.from(new Set(this.collection.items.map(e => e.domain))),
undefined,
naturalCaseCompare
).forEach(e => {
);
if (!domains.length) {
return;
}
this.addItem("-");
domains.forEach(e => {
this.addItem(
e, this.toggleDomainFilter.bind(this, e), this.domains.has(e));
});
@ -673,6 +680,11 @@ export class FilteredCollection extends EventEmitter {
this.emit("sorted");
}
}
invalidateIcons() {
this.items.forEach(item => item.clearFontIcons());
this.recalculate();
}
}
module.exports = {

View File

@ -7,7 +7,7 @@ import { Prefs } from "../../lib/prefs";
import { Keys } from "../keys";
import { $ } from "../winutil";
export default class RemovalModalDialog extends ModalDialog {
export class RemovalModalDialog extends ModalDialog {
private readonly text: string;
private readonly pref: string;
@ -68,3 +68,57 @@ export default class RemovalModalDialog extends ModalDialog {
this.focusDefault();
}
}
export class DeleteFilesDialog extends ModalDialog {
private readonly paths: string[];
constructor(paths: string[]) {
super();
this.paths = paths;
}
async getContent() {
const content = $<HTMLTemplateElement>("#deletefiles-template").
content.cloneNode(true) as DocumentFragment;
await localize(content);
const list = $(".deletefiles-list", content);
for (const path of this.paths) {
const li = document.createElement("li");
li.textContent = path;
list.appendChild(li);
}
return content;
}
get buttons() {
return [
{
title: _("deletefiles_button"),
value: "ok",
default: true,
dismiss: false,
},
{
title: _("cancel"),
value: "cancel",
default: false,
dismiss: true,
}
];
}
async show() {
Keys.suppressed = true;
try {
return await super.show();
}
finally {
Keys.suppressed = false;
}
}
shown() {
this.focusDefault();
}
}

View File

@ -10,6 +10,7 @@ export const StateTexts = locale.then(() => Object.freeze(new Map([
[DownloadState.QUEUED, _("queued")],
[DownloadState.RUNNING, _("running")],
[DownloadState.FINISHING, _("finishing")],
[DownloadState.RETRYING, _("paused")],
[DownloadState.PAUSED, _("paused")],
[DownloadState.DONE, _("done")],
[DownloadState.CANCELED, _("canceled")],
@ -21,6 +22,7 @@ export const StateClasses = Object.freeze(new Map([
[DownloadState.RUNNING, "running"],
[DownloadState.FINISHING, "finishing"],
[DownloadState.PAUSED, "paused"],
[DownloadState.RETRYING, "retrying"],
[DownloadState.DONE, "done"],
[DownloadState.CANCELED, "canceled"],
[DownloadState.MISSING, "missing"],
@ -31,6 +33,7 @@ export const StateIcons = Object.freeze(new Map([
[DownloadState.RUNNING, "icon-go"],
[DownloadState.FINISHING, "icon-go"],
[DownloadState.PAUSED, "icon-pause"],
[DownloadState.RETRYING, "icon-pause"],
[DownloadState.DONE, "icon-done"],
[DownloadState.CANCELED, "icon-error"],
[DownloadState.MISSING, "icon-failed"],

View File

@ -7,7 +7,7 @@ import {
MenuItem,
// eslint-disable-next-line no-unused-vars
SubMenuItem
} from "../../uikit/lib/contextmenu";
} from "../contextmenu";
import { iconForPath } from "../../lib/windowutils";
import { formatSpeed, formatSize, formatTimeDelta } from "../../lib/formatters";
import { filters } from "../../lib/filters";
@ -26,17 +26,22 @@ import {
MenuFilter
} from "./itemfilters";
import { FilteredCollection } from "./itemfilters";
import RemovalModalDialog from "./removaldlg";
import { RemovalModalDialog, DeleteFilesDialog } from "./removaldlg";
import { Stats } from "./stats";
import PORT from "./port";
import { DownloadState, StateTexts, StateClasses, StateIcons } from "./state";
import { Tooltip } from "./tooltip";
import "../../lib/util";
import { CellTypes } from "../../uikit/lib/constants";
import { downloads } from "../../lib/browser";
import { downloads, CHROME } from "../../lib/browser";
import { $ } from "../winutil";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "../../uikit/lib/config";
import { IconCache } from "../../lib/iconcache";
import * as imex from "../../lib/imex";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../../lib/item";
import { API } from "../../lib/api";
const TREE_CONFIG_VERSION = 2;
const RUNNING_TIMEOUT = 1000;
@ -52,7 +57,16 @@ const COL_SPEED = 6;
const COL_MASK = 7;
const COL_SEGS = 8;
const HIDPI = window.matchMedia &&
window.matchMedia("(min-resolution: 2dppx)").matches;
const ICON_BASE_SIZE = 16;
const ICON_REAL_SIZE = !CHROME && HIDPI ? ICON_BASE_SIZE * 2 : ICON_BASE_SIZE;
// eslint-disable-next-line no-magic-numbers
const LARGE_ICON_BASE_SIZE = CHROME ? 32 : 64;
// eslint-disable-next-line no-magic-numbers
const MAX_ICON_BASE_SIZE = CHROME ? 32 : 127;
const LARGE_ICON_REAL_SIZE = HIDPI ? MAX_ICON_BASE_SIZE : LARGE_ICON_BASE_SIZE;
let TEXT_SIZE_UNKNOWM = "unknown";
let REAL_STATE_TEXTS = Object.freeze(new Map<number, string>());
@ -106,7 +120,9 @@ export class DownloadItem extends EventEmitter {
public error: string;
public finalName: string;
public currentName: string;
public ext?: string;
public position: number;
@ -128,6 +144,14 @@ export class DownloadItem extends EventEmitter {
public mask: string;
private iconField?: string;
private largeIconField?: string;
public opening: boolean;
public retries: number;
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
super();
Object.assign(this, raw);
@ -138,6 +162,42 @@ export class DownloadItem extends EventEmitter {
this.lastWritten = 0;
}
get icon() {
if (this.iconField) {
return this.iconField;
}
this.iconField = this.owner.icons.get(
iconForPath(this.currentName, ICON_BASE_SIZE));
if (this.ext) {
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
if (icon) {
this.iconField = this.owner.icons.get(icon);
if (typeof this.filteredPosition !== undefined) {
this.owner.invalidateCell(this.filteredPosition, COL_URL);
}
}
});
}
return this.iconField || "";
}
get largeIcon() {
if (this.largeIconField) {
return this.largeIconField;
}
this.largeIconField = this.owner.icons.get(
iconForPath(this.currentName, LARGE_ICON_BASE_SIZE));
if (this.ext) {
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
if (icon) {
this.largeIconField = this.owner.icons.get(icon);
}
this.emit("largeIcon");
});
}
return this.largeIconField || "";
}
get eta() {
const {avg} = this.stats;
if (!this.totalSize || !avg) {
@ -165,7 +225,7 @@ export class DownloadItem extends EventEmitter {
if (this.owner.showUrls.value) {
return this.usable;
}
return this.finalName;
return this.currentName;
}
get fmtSize() {
@ -193,6 +253,12 @@ export class DownloadItem extends EventEmitter {
if (this.state === DownloadState.RUNNING) {
return this.eta;
}
if (this.state === DownloadState.RETRYING) {
if (this.error) {
return _("retrying_error", _(this.error) || this.error);
}
return _("retrying");
}
if (this.error) {
return _(this.error) || this.error;
}
@ -215,6 +281,9 @@ export class DownloadItem extends EventEmitter {
PORT.post("all");
return;
}
if (("ext" in raw) && raw.ext !== this.ext) {
this.clearIcons();
}
delete raw.position;
delete raw.owner;
const oldState = this.state;
@ -297,6 +366,20 @@ export class DownloadItem extends EventEmitter {
this.domain = this.uURL.domain;
this.emit("url");
}
clearIcons() {
this.iconField = undefined;
this.largeIconField = undefined;
}
clearFontIcons() {
if (this.iconField && this.iconField.startsWith("icon-")) {
this.iconField = undefined;
}
if (this.largeIconField && this.largeIconField.startsWith("icon-")) {
this.largeIconField = undefined;
}
}
}
@ -323,7 +406,7 @@ export class DownloadTable extends VirtualTable {
private readonly sids: Map<number, DownloadItem>;
private readonly icons: Icons;
public readonly icons: Icons;
private readonly contextMenu: ContextMenu;
@ -333,6 +416,8 @@ export class DownloadTable extends VirtualTable {
private readonly openDirectoryAction: Broadcaster;
private readonly deleteFilesAction: Broadcaster;
private readonly moveTopAction: Broadcaster;
private readonly moveUpAction: Broadcaster;
@ -357,6 +442,7 @@ export class DownloadTable extends VirtualTable {
this.showUrls = new ShowUrlsWatcher(this);
this.updateCounts = debounce(this.updateCounts.bind(this), 100);
this.onIconCached = debounce(this.onIconCached.bind(this), 1000);
this.downloads = new FilteredCollection(this);
this.downloads.on("changed", () => this.updateCounts());
@ -407,6 +493,8 @@ export class DownloadTable extends VirtualTable {
col.iconElem.classList.remove("icon-filter");
});
IconCache.on("cached", this.onIconCached.bind(this));
this.sids = new Map<number, DownloadItem>();
this.icons = new Icons($("#icons"));
@ -450,8 +538,16 @@ export class DownloadTable extends VirtualTable {
return true;
});
Keys.on("SHIFT-Delete", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.removeCompleteDownloads(false);
return true;
});
ctx.on("ctx-remove-all", () => this.removeAllDownloads());
ctx.on("ctx-remove-complete", () => this.removeCompleteDownloads(false));
ctx.on("ctx-remove-complete-all",
() => this.removeCompleteDownloads(false));
ctx.on("ctx-remove-complete-selected",
@ -464,6 +560,12 @@ export class DownloadTable extends VirtualTable {
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
ctx.on("ctx-import", () => this.importDownloads());
ctx.on("ctx-export-text", () => this.exportDownloads(imex.textExporter));
ctx.on("ctx-export-aria2", () => this.exportDownloads(imex.aria2Exporter));
ctx.on("ctx-export-metalink",
() => this.exportDownloads(imex.metalinkExporter));
ctx.on("dismissed", () => this.table.focus());
this.on("contextmenu", (tree, event) => {
@ -497,6 +599,9 @@ export class DownloadTable extends VirtualTable {
this.openDirectoryAction = new Broadcaster("ctx-open-directory");
this.openDirectoryAction.onaction = this.openDirectory.bind(this);
this.deleteFilesAction = new Broadcaster("ctx-delete-files");
this.deleteFilesAction.onaction = this.deleteFiles.bind(this);
const moveAction = (method: string) => {
if (this.selection.empty) {
return;
@ -528,6 +633,7 @@ export class DownloadTable extends VirtualTable {
this.moveBottomAction,
this.openFileAction,
this.openDirectoryAction,
this.deleteFilesAction,
]);
this.on(
@ -535,8 +641,12 @@ export class DownloadTable extends VirtualTable {
this.selection.clear();
this.tooltip = null;
this.on("hover", async info => {
if (!(await Prefs.get("tooltip"))) {
const tooltipWatcher = new PrefWatcher("tooltip", true);
this.on("hover", info => {
if (!document.hasFocus()) {
return;
}
if (!tooltipWatcher.value) {
return;
}
const item = this.downloads.filtered[info.rowid];
@ -665,6 +775,7 @@ export class DownloadTable extends VirtualTable {
}
selectionChanged() {
this.dismissTooltip();
const {empty} = this.selection;
if (empty) {
for (const d of this.disableSet) {
@ -695,6 +806,10 @@ export class DownloadTable extends VirtualTable {
this.cancelAction.disabled = true;
}
if (!(states & DownloadState.DONE)) {
this.deleteFilesAction.disabled = true;
}
const item = this.focusRow >= 0 ?
this.downloads.filtered[this.focusRow] :
null;
@ -705,7 +820,8 @@ export class DownloadTable extends VirtualTable {
}
resumeDownloads(forced = false) {
const sids = this.getSelectedSids(DownloadState.RESUMABLE);
const sids = this.getSelectedSids(
forced ? DownloadState.FORCABLE : DownloadState.RESUMABLE);
if (!sids.length) {
return;
}
@ -729,20 +845,30 @@ export class DownloadTable extends VirtualTable {
}
async openFile() {
if (this.focusRow < 0) {
this.dismissTooltip();
const {focusRow} = this;
if (focusRow < 0) {
return;
}
const item = this.downloads.filtered[this.focusRow];
const item = this.downloads.filtered[focusRow];
if (!item || !item.manId || item.state !== DownloadState.DONE) {
return;
}
item.opening = true;
try {
this.invalidateRow(focusRow);
await downloads.open(item.manId);
}
catch (ex) {
console.error(ex, ex.toString(), ex);
PORT.post("missing", {sid: item.sessionId});
}
finally {
setTimeout(() => {
item.opening = false;
this.invalidateRow(focusRow);
}, 500);
}
}
async openDirectory() {
@ -762,6 +888,33 @@ export class DownloadTable extends VirtualTable {
}
}
async deleteFiles() {
const items = [];
for (const rowid of this.selection) {
const item = this.downloads.filtered[rowid];
if (item.state === DownloadState.DONE && item.manId) {
items.push(item);
}
}
if (!items.length) {
return;
}
const sids = items.map(i => i.sessionId);
const paths = items.map(i => i.destFull);
await new DeleteFilesDialog(paths).show();
await Promise.all(items.map(async item => {
try {
if (item.manId && item.state === DownloadState.DONE) {
await downloads.removeFile(item.manId);
}
}
catch {
// ignored
}
}));
this.removeDownloadsInternal(sids);
}
removeDownloadsInternal(sids?: number[]) {
if (!sids) {
sids = [];
@ -993,10 +1146,16 @@ export class DownloadTable extends VirtualTable {
this.updateSizes();
$("#statusSpeedContainer").classList.remove("hidden");
}
if (item.manId && item.ext) {
IconCache.set(item.ext, item.manId).catch(console.error);
}
break;
case DownloadState.DONE:
this.finished++;
if (item.manId && item.ext) {
IconCache.set(item.ext, item.manId).catch(console.error);
}
break;
}
this.selectionChanged();
@ -1028,12 +1187,64 @@ export class DownloadTable extends VirtualTable {
this.selection.toggle(0, this.rowCount - 1);
}
importDownloads() {
const picker = document.createElement("input");
picker.setAttribute("type", "file");
picker.setAttribute("accept", "text/*,.txt,.lst,.metalink,.meta4");
picker.onchange = () => {
if (!picker.files || !picker.files.length) {
return;
}
const reader = new FileReader();
reader.onload = () => {
if (!reader.result) {
return;
}
const items = imex.importText(reader.result as string);
if (!items || !items.length) {
return;
}
API.regular(items, []);
};
reader.readAsText(picker.files[0], "utf-8");
};
picker.click();
}
exportDownloads(exporter: imex.Exporter) {
const items = this.getSelectedItems();
if (!items.length) {
return;
}
const text = exporter.getText(items as unknown as BaseItem[]);
const enc = new TextEncoder();
const data = enc.encode(text);
const url = URL.createObjectURL(new Blob([data], {type: "text/plain"}));
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", exporter.fileName);
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
getRowClasses(rowid: number) {
const item = this.downloads.filtered[rowid];
if (!item) {
return null;
}
if (item.opening) {
return ["opening"];
}
const cls = StateClasses.get(item.state);
if (cls && item.opening) {
return [cls, "opening"];
}
if (item.opening) {
return ["opening"];
}
return cls && [cls] || null;
}
@ -1043,10 +1254,10 @@ export class DownloadTable extends VirtualTable {
}
const item = this.downloads.filtered[rowid];
if (colid === COL_URL) {
return this.icons.get(iconForPath(item.finalName, ICON_BASE_SIZE));
return item.icon;
}
if (colid === COL_PROGRESS) {
return StateIcons.get(item.state);
return StateIcons.get(item.state) || null;
}
return null;
}
@ -1119,4 +1330,8 @@ export class DownloadTable extends VirtualTable {
return -1;
}
}
onIconCached() {
this.downloads.invalidateIcons();
}
}

View File

@ -146,6 +146,8 @@ export class Tooltip {
constructor(item: DownloadItem, pos: number) {
this.update = this.update.bind(this);
this.item = item;
this.item.on("largeIcon", this.update);
const tmpl = (
document.querySelector<HTMLTemplateElement>("#tooltip-template"));
if (!tmpl) {
@ -178,7 +180,7 @@ export class Tooltip {
this.dismiss();
return;
}
const icon = item.owner.getCellIcon(item.filteredPosition, 0);
const icon = item.largeIcon;
this.icon.className = icon;
this.name.textContent = item.destFull;
this.from.textContent = item.usable;
@ -190,7 +192,7 @@ export class Tooltip {
const hidden = this.speedbox.classList.contains("hidden");
if (!running && !hidden) {
this.eta.style.fontWeight = "bold";
this.eta.classList.add("single");
this.etalabel.classList.add("hidden");
this.speedbox.classList.add("hidden");
this.progressbar.classList.add("hidden");
@ -200,7 +202,7 @@ export class Tooltip {
return;
}
if (hidden) {
this.eta.style.fontWeight = "auto";
this.eta.classList.remove("single");
this.etalabel.classList.remove("hidden");
this.speedbox.classList.remove("hidden");
this.progressbar.classList.remove("hidden");
@ -318,5 +320,6 @@ export class Tooltip {
}
this.item.off("stats", this.update);
this.item.off("update", this.update);
this.item.off("largeIcon", this.update);
}
}

View File

@ -7,6 +7,12 @@
box-sizing: content-box !important;
}
html, body {
height: auto !important;
-webkit-user-select: none;
user-select: none;
}
ul {
margin: 1.5ex;
margin-right: 2ex;
@ -25,6 +31,8 @@
vertical-align: center;
align-items: center;
border-radius: 4px;
cursor: default;
white-space: nowrap;
}
li.sep {

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