Compare commits
163 Commits
Author | SHA1 | Date | |
---|---|---|---|
7ee13af238 | |||
d488e5874a | |||
b1a7c22452 | |||
e928d202ee | |||
c39961d253 | |||
c6d11fcd7f | |||
eb96103478 | |||
583ccfc7b1 | |||
e0437718a0 | |||
2126ae022b | |||
2ef39dcb19 | |||
047c865e76 | |||
c586cd00cc | |||
ee7f470269 | |||
f04dda308b | |||
071458e262 | |||
9ffc96de4d | |||
26e9a5404a | |||
f44fe59054 | |||
e4b0629dee | |||
5c2700ca36 | |||
639a582804 | |||
2d1f185fcd | |||
38735ed0ae | |||
216bc590da | |||
1c10d8005a | |||
1fcfbe5360 | |||
8d3dda1cec | |||
be18f667d9 | |||
027b2c4fb1 | |||
4ed92878be | |||
a6930f309e | |||
fdcdae0412 | |||
2c18ddaaa8 | |||
994e7ad0a6 | |||
95536b36be | |||
9c159d5d24 | |||
42ccfd5dc5 | |||
dabf7f8a28 | |||
9cac48f439 | |||
ef6bc840d8 | |||
1c38ec1357 | |||
a4436bd6c8 | |||
5a4b8143b2 | |||
00a5712427 | |||
6e55ee0745 | |||
56098a382e | |||
5ba9c7179b | |||
be9256ff1f | |||
92b2c32dd3 | |||
1935c7f444 | |||
4d48a2c395 | |||
3cf30aaf08 | |||
262c3e169b | |||
2a0cb6720f | |||
295077dd75 | |||
c484f82cf5 | |||
05aaac7ed8 | |||
9ef497028c | |||
380325bd43 | |||
164aa99eca | |||
0a9155dcec | |||
0bf9e76441 | |||
392681c1b7 | |||
d4024a16ad | |||
fba985482c | |||
094fb0ee84 | |||
2ccc12de90 | |||
28e6866db8 | |||
7185964649 | |||
cd1005823d | |||
0afceb9850 | |||
e6dc205b9d | |||
8e473c778b | |||
7a71ae5f37 | |||
d04f3db22f | |||
adc6b9dbb2 | |||
e8f09c80f3 | |||
46c4e66558 | |||
a8ea416a67 | |||
320c1ddafa | |||
4c576ba720 | |||
7b58779f9e | |||
45d835fe19 | |||
2d2826d192 | |||
4c77ad0f1f | |||
4d72ac4534 | |||
cdda0835d8 | |||
78e91304eb | |||
0702631003 | |||
c45bf671fb | |||
33a3e275fc | |||
369514f155 | |||
494479ce1a | |||
98ebb160f9 | |||
d1cc406f05 | |||
687b6e1aa9 | |||
c960d48b72 | |||
c8c7506efc | |||
91edcee28c | |||
a425a786ef | |||
f023351acc | |||
c8610eee29 | |||
f7a70ec2ea | |||
69d8ffe8a5 | |||
71240ec1e8 | |||
a6f3c7a647 | |||
eab9631a11 | |||
9e29db911c | |||
8d1040115a | |||
3551545eae | |||
01001dc7b2 | |||
b4e6ab80d2 | |||
d0f6c4f7f3 | |||
5b05d52886 | |||
07a3ec3a7b | |||
0f15a4a068 | |||
c1e5f63935 | |||
6e4a338789 | |||
37a97b73b3 | |||
fe61f6176c | |||
48bde9cfdb | |||
256c091f15 | |||
b4ce2d1d75 | |||
29fd59c8fd | |||
544b7d522c | |||
116d5b9b00 | |||
976c57c043 | |||
d00b25cbe7 | |||
572bab27a0 | |||
8235af22db | |||
2bfb3d5363 | |||
7dc4dd9da6 | |||
c7c111e1c0 | |||
a9a811d96b | |||
801eaa819b | |||
d6539c5f96 | |||
a89acad0c9 | |||
70c7e0b0f3 | |||
af8b05d20c | |||
d98c4e318a | |||
176060183e | |||
26cd8e8a00 | |||
a9df98c2f6 | |||
ab2b6e40af | |||
112d37deb0 | |||
33bde621f9 | |||
958d58a408 | |||
229b5eb968 | |||
2f282d3a4b | |||
a9f83071dc | |||
7bfffd7598 | |||
545d78ad61 | |||
0463471704 | |||
34ea21b3ea | |||
d3c9f8bc89 | |||
4236195ccf | |||
10ff6f1c11 | |||
357a2bdfcc | |||
3eaa7ed822 | |||
056c164659 | |||
7b2d07e1f5 | |||
2d1a7eceb0 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
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**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
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**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
23
LICENSE.md
@ -30,7 +30,7 @@ THE SOFTWARE.
|
|||||||
## DownThemAll! uikit
|
## DownThemAll! uikit
|
||||||
|
|
||||||
Copyright © 2016-2019 by Nils Maier
|
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)
|
## 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
|
## DownThemAll! icons, icon-font and graphic assets
|
||||||
|
|
||||||
Copyright (C) 2012-2019 by Nils Maier
|
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.
|
The icon font contains icons from Font Awesome.
|
||||||
|
|
||||||
See: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
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
|
The DownThemAll! name and logo cannot be used without explicit permission
|
||||||
in any derivative work, except in credits and license-related notices.
|
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
|
modifications of the software and forks is permitted without explicit
|
||||||
permission.
|
permission.
|
||||||
|
|
||||||
|
Distributing official DownThemAll! releases without any modifications is allowed without explicit permission.
|
||||||
|
|
||||||
## Font Awesome
|
## Font Awesome
|
||||||
|
|
||||||
Copyright (C) 2016 by Dave Gandy
|
Copyright (C) 2016 by Dave Gandy
|
||||||
|
|
||||||
License: SIL ()
|
License: SIL ()
|
||||||
|
|
||||||
Homepage: http://fortawesome.github.com/Font-Awesome/
|
Homepage: http://fortawesome.github.com/Font-Awesome/
|
||||||
|
|
||||||
## webextension-polyfill
|
## webextension-polyfill
|
||||||
|
|
||||||
Lcensed under the Mozilla Public License 2.0.
|
Licensed under the Mozilla Public License 2.0.
|
||||||
|
|
||||||
## PSL (public-suffix-list)
|
## PSL (public-suffix-list)
|
||||||
|
|
||||||
The list itself is licensed under the Mozilla Public License 2.0.
|
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)
|
||||||
|
51
Readme.md
@ -1,12 +1,10 @@
|
|||||||
DownThemAll! WE
|
# DownThemAll! WE
|
||||||
===
|
|
||||||
|
|
||||||
The DownThemAll! WebExtension.
|
The DownThemAll! WebExtension.
|
||||||
|
|
||||||
For those still on supported browser: [Non-WebExtension legacy code](https://github.com/downthemall/downthemall-legacy).
|
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.
|
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.
|
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,19 +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.**
|
**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
|
||||||
|
|
||||||
|
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.
|
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.
|
Please note: You have to run `yarn watch` (at least once) as it builds the actual script bundles.
|
||||||
* `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).
|
|
||||||
|
|
||||||
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).
|
### Firefox
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
The code base is comparatively large for a WebExtension, with over 10K sloc of typescript and over 14K sloc total.
|
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 with the right about:config preferences).
|
||||||
|
|
||||||
|
### Chrome
|
||||||
|
|
||||||
|
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
|
||||||
|
16
TODO.md
@ -3,20 +3,11 @@ TODO
|
|||||||
|
|
||||||
aka a lot
|
aka a lot
|
||||||
|
|
||||||
P1
|
|
||||||
===
|
|
||||||
|
|
||||||
Musts.
|
|
||||||
|
|
||||||
* packaging
|
|
||||||
* signing
|
|
||||||
|
|
||||||
P2
|
P2
|
||||||
===
|
===
|
||||||
|
|
||||||
Planned for later.
|
Planned for later.
|
||||||
|
|
||||||
* Investigate using an action popup for the browser action
|
|
||||||
* Soft errors and retry logic
|
* 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.
|
* 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)
|
* Delete files (well, as far as the browser allows)
|
||||||
@ -24,10 +15,6 @@ Planned for later.
|
|||||||
* Add downloads
|
* Add downloads
|
||||||
* Chrome support
|
* Chrome support
|
||||||
* vtable perf: cache column widths
|
* vtable perf: cache column widths
|
||||||
* Localizations
|
|
||||||
* Settle on system
|
|
||||||
* Do the de-locale
|
|
||||||
* Enagage translators
|
|
||||||
* Download options
|
* 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.
|
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
||||||
|
|
||||||
@ -48,7 +35,6 @@ Nice-to-haves.
|
|||||||
* Dark Theme support
|
* Dark Theme support
|
||||||
* os/browser define be default
|
* os/browser define be default
|
||||||
* overwritable
|
* 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)
|
* Remove `any` types as possible, and generally improve typescript (new language to me)
|
||||||
|
|
||||||
P4
|
P4
|
||||||
@ -66,8 +52,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
|
|||||||
* Not supported by Firefox
|
* Not supported by Firefox
|
||||||
* Speed limiter
|
* Speed limiter
|
||||||
* Cannot be done with the WebExtensions downloads API
|
* Cannot be done with the WebExtensions downloads API
|
||||||
* Actually send referrers for downloads
|
|
||||||
* Cannot be done with WebExtensions - webRequest does not see Downloads
|
|
||||||
* contenthandling aka video sniffing, request manipulation?
|
* contenthandling aka video sniffing, request manipulation?
|
||||||
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
|
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
|
||||||
* Checksums/Hashes?
|
* Checksums/Hashes?
|
||||||
|
33
_locales/Readme.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Translations
|
||||||
|
|
||||||
|
Right now we did not standardize on a tool/website/community use for translations
|
||||||
|
|
||||||
|
## 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. 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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.
|
19
_locales/all.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"cs": "Čeština (CZ) [cs]",
|
||||||
|
"de": "Deutsch [de]",
|
||||||
|
"el": "Ελληνικά [el]",
|
||||||
|
"en": "English (US) [en]",
|
||||||
|
"es": "Español (España) [es]",
|
||||||
|
"et": "Eesti Keel [et]",
|
||||||
|
"fr": "Français (FR) [fr]",
|
||||||
|
"hu": "Magyar (HU) [hu]",
|
||||||
|
"id": "Bahasa Indonesia [id]",
|
||||||
|
"ko": "한국어 [ko]",
|
||||||
|
"lt": "Lietuvių [lt]",
|
||||||
|
"nl": "Nederlands [nl]",
|
||||||
|
"pl": "Polski (PL) [pl]",
|
||||||
|
"pt": "Português (Brasil) [pt]",
|
||||||
|
"ru": "Русский [ru]",
|
||||||
|
"zh_CN": "简体中文 [zh_CN]",
|
||||||
|
"zh_TW": "正體中文 [zh_TW]"
|
||||||
|
}
|
1170
_locales/cs/messages.json
Normal file
1170
_locales/de/messages.json
Normal file
1170
_locales/el/messages.json
Normal file
1170
_locales/es/messages.json
Normal file
1170
_locales/et/messages.json
Normal file
1178
_locales/fr/messages.json
Normal file
1170
_locales/hu/messages.json
Normal file
1018
_locales/id/messages.json
Normal file
1170
_locales/ko/messages.json
Normal file
1170
_locales/lt/messages.json
Normal file
1170
_locales/nl/messages.json
Normal file
1170
_locales/pl/messages.json
Executable file
1160
_locales/pt/messages.json
Normal file
1170
_locales/ru/messages.json
Normal file
1170
_locales/zh_CN/messages.json
Normal file
1170
_locales/zh_TW/messages.json
Normal file
@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"deffilter-aud": {
|
"deffilter-aud": {
|
||||||
"label": "Audio",
|
"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,
|
"type": 1,
|
||||||
"active": false,
|
"active": false,
|
||||||
"icon": "mp3"
|
"icon": "mp3"
|
||||||
@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"deffilter-img": {
|
"deffilter-img": {
|
||||||
"label": "Images",
|
"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,
|
"type": 3,
|
||||||
"active": false,
|
"active": false,
|
||||||
"icon": "jpg"
|
"icon": "jpg"
|
||||||
@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"deffilter-vid": {
|
"deffilter-vid": {
|
||||||
"label": "Videos",
|
"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,
|
"type": 3,
|
||||||
"active": true,
|
"active": true,
|
||||||
"icon": "mkv"
|
"icon": "mkv"
|
||||||
|
@ -80,7 +80,11 @@
|
|||||||
"tif",
|
"tif",
|
||||||
"tiff",
|
"tiff",
|
||||||
"wmf",
|
"wmf",
|
||||||
"webp"
|
"webp",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"jxr",
|
||||||
|
"wdp"
|
||||||
],
|
],
|
||||||
"video": [
|
"video": [
|
||||||
"3g2",
|
"3g2",
|
||||||
|
396
data/mime.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,10 @@
|
|||||||
"open-manager-on-queue": true,
|
"open-manager-on-queue": true,
|
||||||
"text-links": true,
|
"text-links": true,
|
||||||
"add-paused": false,
|
"add-paused": false,
|
||||||
|
"hide-context": false,
|
||||||
"conflict-action": "uniquify",
|
"conflict-action": "uniquify",
|
||||||
"nagging": 0,
|
"nagging": 0,
|
||||||
"nagging-next": 6,
|
"nagging-next": 7,
|
||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"show-urls": false,
|
"show-urls": false,
|
||||||
"remove-missing-on-init": false,
|
"remove-missing-on-init": false,
|
||||||
|
68
lib/api.ts
@ -5,7 +5,8 @@ import { TYPE_LINK, TYPE_MEDIA } from "./constants";
|
|||||||
import { filters } from "./filters";
|
import { filters } from "./filters";
|
||||||
import { Prefs } from "./prefs";
|
import { Prefs } from "./prefs";
|
||||||
import { lazy } from "./util";
|
import { lazy } from "./util";
|
||||||
import { Item, makeUniqueItems } from "./item";
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Item, makeUniqueItems, BaseItem } from "./item";
|
||||||
import { getManager } from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
import { select } from "./select";
|
import { select } from "./select";
|
||||||
import { single } from "./single";
|
import { single } from "./single";
|
||||||
@ -16,12 +17,17 @@ import { _ } from "./i18n";
|
|||||||
|
|
||||||
const MAX_BATCH = 10000;
|
const MAX_BATCH = 10000;
|
||||||
|
|
||||||
export const API = new class {
|
export interface QueueOptions {
|
||||||
async filter(arr: any, type: number) {
|
mask?: string;
|
||||||
return (await filters()).filterItemsByType(arr, type);
|
paused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API = new class APIImpl {
|
||||||
|
async filter(arr: BaseItem[], type: number) {
|
||||||
|
return await (await filters()).filterItemsByType(arr, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queue(items: any, options: any) {
|
async queue(items: BaseItem[], options: QueueOptions) {
|
||||||
await MASK.init();
|
await MASK.init();
|
||||||
const {mask = MASK.current} = options;
|
const {mask = MASK.current} = options;
|
||||||
|
|
||||||
@ -36,12 +42,9 @@ export const API = new class {
|
|||||||
fileName: null,
|
fileName: null,
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
fromMetalink: false,
|
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
hashes: [],
|
|
||||||
private: false,
|
private: false,
|
||||||
postData: null,
|
postData: null,
|
||||||
cleanRequest: false,
|
|
||||||
mask,
|
mask,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
paused
|
paused
|
||||||
@ -54,7 +57,7 @@ export const API = new class {
|
|||||||
}
|
}
|
||||||
return currentBatch;
|
return currentBatch;
|
||||||
});
|
});
|
||||||
items = items.map((i: any) => {
|
items = items.map(i => {
|
||||||
delete i.idx;
|
delete i.idx;
|
||||||
return new Item(i, defaults);
|
return new Item(i, defaults);
|
||||||
});
|
});
|
||||||
@ -79,7 +82,7 @@ export const API = new class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sanity(links: any[], media: any[]) {
|
sanity(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!links.length && !media.length) {
|
if (!links.length && !media.length) {
|
||||||
new Notification(null, _("no-links"));
|
new Notification(null, _("no-links"));
|
||||||
return false;
|
return false;
|
||||||
@ -87,48 +90,53 @@ export const API = new class {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async turbo(links: any[], media: any[]) {
|
async turbo(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!this.sanity(links, media)) {
|
if (!this.sanity(links, media)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selected = makeUniqueItems([
|
const type = await Prefs.get("last-type", "links");
|
||||||
await API.filter(links, TYPE_LINK),
|
const items = await (async () => {
|
||||||
await API.filter(media, TYPE_MEDIA),
|
if (type === "links") {
|
||||||
]);
|
return await API.filter(links, TYPE_LINK);
|
||||||
|
}
|
||||||
|
return await API.filter(media, TYPE_MEDIA);
|
||||||
|
})();
|
||||||
|
const selected = makeUniqueItems([items]);
|
||||||
if (!selected.length) {
|
if (!selected.length) {
|
||||||
return await this.regular(links, media);
|
return await this.regular(links, media);
|
||||||
}
|
}
|
||||||
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
|
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
|
||||||
}
|
}
|
||||||
|
|
||||||
async regularInternal(selected: any) {
|
async regularInternal(selected: BaseItem[], options: any) {
|
||||||
if (selected.mask && !selected.maskOnce) {
|
if (options.mask && !options.maskOnce) {
|
||||||
await MASK.init();
|
await MASK.init();
|
||||||
await MASK.push(selected.mask);
|
await MASK.push(options.mask);
|
||||||
}
|
}
|
||||||
if (typeof selected.fast === "string" && !selected.fastOnce) {
|
if (typeof options.fast === "string" && !options.fastOnce) {
|
||||||
await FASTFILTER.init();
|
await FASTFILTER.init();
|
||||||
await FASTFILTER.push(selected.fast);
|
await FASTFILTER.push(options.fast);
|
||||||
}
|
}
|
||||||
const {items} = selected;
|
if (typeof options.type === "string") {
|
||||||
delete selected.items;
|
await Prefs.set("last-type", options.type);
|
||||||
return await this.queue(items, selected);
|
}
|
||||||
|
return await this.queue(selected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async regular(links: any[], media: any[]) {
|
async regular(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!this.sanity(links, media)) {
|
if (!this.sanity(links, media)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selected = await select(links, media);
|
const {items, options} = await select(links, media);
|
||||||
return this.regularInternal(selected);
|
return this.regularInternal(items, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async singleTurbo(item: any) {
|
async singleTurbo(item: BaseItem) {
|
||||||
return await this.queue([item], {paused: await Prefs.get("add-paused")});
|
return await this.queue([item], {paused: await Prefs.get("add-paused")});
|
||||||
}
|
}
|
||||||
|
|
||||||
async singleRegular(item: any) {
|
async singleRegular(item: BaseItem | null) {
|
||||||
const selected = await single(item);
|
const {items, options} = await single(item);
|
||||||
return this.regularInternal(selected);
|
return this.regularInternal(items, options);
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
@ -5,7 +5,7 @@ import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
|
|||||||
import { API } from "./api";
|
import { API } from "./api";
|
||||||
import { Finisher, makeUniqueItems } from "./item";
|
import { Finisher, makeUniqueItems } from "./item";
|
||||||
import { Prefs } from "./prefs";
|
import { Prefs } from "./prefs";
|
||||||
import { _ } from "./i18n";
|
import { _, locale } from "./i18n";
|
||||||
import { openPrefs, openManager } from "./windowutils";
|
import { openPrefs, openManager } from "./windowutils";
|
||||||
import { filters } from "./filters";
|
import { filters } from "./filters";
|
||||||
import { getManager } from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
@ -13,13 +13,40 @@ import {
|
|||||||
browserAction as action,
|
browserAction as action,
|
||||||
menus as _menus, contextMenus as _cmenus,
|
menus as _menus, contextMenus as _cmenus,
|
||||||
tabs,
|
tabs,
|
||||||
webNavigation as nav
|
webNavigation as nav,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Tab,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MenuClickInfo,
|
||||||
|
CHROME,
|
||||||
|
runtime,
|
||||||
|
history,
|
||||||
|
sessions,
|
||||||
} from "./browser";
|
} from "./browser";
|
||||||
|
import { Bus } from "./bus";
|
||||||
|
import { filterInSitu } from "./util";
|
||||||
|
|
||||||
|
|
||||||
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
||||||
|
|
||||||
|
const GATHER = "/bundles/content-gather.js";
|
||||||
|
|
||||||
async function runContentJob(tab: any, file: string, msg: any) {
|
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 {
|
try {
|
||||||
const res = await tabs.executeScript(tab.id, {
|
const res = await tabs.executeScript(tab.id, {
|
||||||
file,
|
file,
|
||||||
@ -48,6 +75,14 @@ async function runContentJob(tab: any, file: string, msg: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectionOptions = {
|
||||||
|
selectionOnly: boolean;
|
||||||
|
allTabs: boolean;
|
||||||
|
turbo: boolean;
|
||||||
|
tab: Tab;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class Handler {
|
class Handler {
|
||||||
async processResults(turbo = false, results: any[]) {
|
async processResults(turbo = false, results: any[]) {
|
||||||
const links = this.makeUnique(results, "links");
|
const links = this.makeUnique(results, "links");
|
||||||
@ -59,14 +94,46 @@ class Handler {
|
|||||||
return makeUniqueItems(
|
return makeUniqueItems(
|
||||||
results.filter(e => e[what]).map(e => {
|
results.filter(e => e[what]).map(e => {
|
||||||
const finisher = new Finisher(e);
|
const finisher = new Finisher(e);
|
||||||
return e[what].
|
return filterInSitu(e[what].
|
||||||
map((item: any) => finisher.finish(item)).
|
map((item: any) => finisher.finish(item)), e => !!e);
|
||||||
filter((i: any) => i);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async performSelection(options: SelectionOptions) {
|
||||||
|
try {
|
||||||
|
const toptions: any = {
|
||||||
|
currentWindow: true,
|
||||||
|
discarded: false,
|
||||||
|
};
|
||||||
|
if (!CHROME) {
|
||||||
|
toptions.hidden = false;
|
||||||
|
}
|
||||||
|
const selectedTabs = options.allTabs ?
|
||||||
|
await tabs.query(toptions) as any[] :
|
||||||
|
[options.tab];
|
||||||
|
|
||||||
|
const textLinks = await Prefs.get("text-links", true);
|
||||||
|
const goptions = {
|
||||||
|
type: "DTA:gather",
|
||||||
|
selectionOnly: options.selectionOnly,
|
||||||
|
textLinks,
|
||||||
|
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||||
|
transferable: TRANSFERABLE_PROPERTIES,
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await Promise.all(selectedTabs.
|
||||||
|
map((tab: any) => runContentJob(tab, GATHER, goptions)));
|
||||||
|
|
||||||
|
await this.processResults(options.turbo, results.flat());
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error(ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new class Action extends Handler {
|
locale.then(() => {
|
||||||
|
new class Action extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onClicked = this.onClicked.bind(this);
|
this.onClicked = this.onClicked.bind(this);
|
||||||
@ -79,7 +146,7 @@ new class Action extends Handler {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.processResults(
|
await this.processResults(
|
||||||
await Prefs.get("global-turbo"),
|
true,
|
||||||
await runContentJob(
|
await runContentJob(
|
||||||
tab, "/bundles/content-gather.js", {
|
tab, "/bundles/content-gather.js", {
|
||||||
type: "DTA:gather",
|
type: "DTA:gather",
|
||||||
@ -93,31 +160,28 @@ new class Action extends Handler {
|
|||||||
console.error(ex);
|
console.error(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
new class Menus extends Handler {
|
const menuHandler = new class Menus extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onClicked = this.onClicked.bind(this);
|
this.onClicked = this.onClicked.bind(this);
|
||||||
menus.create({
|
const alls = new Map<string, string[]>();
|
||||||
id: "DTARegular",
|
const mcreate = (options: any) => {
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
if (CHROME) {
|
||||||
icons: {
|
delete options.icons;
|
||||||
16: "/style/button-regular.png",
|
options.contexts = options.contexts.
|
||||||
32: "/style/button-regular@2x.png",
|
filter((e: string) => CHROME_CONTEXTS.has(e));
|
||||||
},
|
if (!options.contexts.length) {
|
||||||
title: _("dta.regular"),
|
return;
|
||||||
});
|
}
|
||||||
menus.create({
|
}
|
||||||
id: "DTATurbo",
|
if (options.contexts.includes("all")) {
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
alls.set(options.id, options.contexts);
|
||||||
icons: {
|
}
|
||||||
16: "/style/button-turbo.png",
|
menus.create(options);
|
||||||
32: "/style/button-turbo@2x.png",
|
};
|
||||||
},
|
mcreate({
|
||||||
title: _("dta.turbo"),
|
|
||||||
});
|
|
||||||
menus.create({
|
|
||||||
id: "DTARegularLink",
|
id: "DTARegularLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -126,7 +190,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.link"),
|
title: _("dta.regular.link"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboLink",
|
id: "DTATurboLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -135,7 +199,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.link"),
|
title: _("dta.turbo.link"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularImage",
|
id: "DTARegularImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -144,7 +208,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.image"),
|
title: _("dta.regular.image"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboImage",
|
id: "DTATurboImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -153,7 +217,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.image"),
|
title: _("dta.turbo.image"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularMedia",
|
id: "DTARegularMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -162,7 +226,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.media"),
|
title: _("dta.regular.media"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboMedia",
|
id: "DTATurboMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -171,7 +235,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.media"),
|
title: _("dta.turbo.media"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularSelection",
|
id: "DTARegularSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -180,7 +244,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.selection"),
|
title: _("dta.regular.selection"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboSelection",
|
id: "DTATurboSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -189,12 +253,72 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.selection"),
|
title: _("dta.turbo.selection"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
|
id: "DTARegular",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
icons: {
|
||||||
|
16: "/style/button-regular.png",
|
||||||
|
32: "/style/button-regular@2x.png",
|
||||||
|
},
|
||||||
|
title: _("dta.regular"),
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
|
id: "DTATurbo",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
icons: {
|
||||||
|
16: "/style/button-turbo.png",
|
||||||
|
32: "/style/button-turbo@2x.png",
|
||||||
|
},
|
||||||
|
title: _("dta.turbo"),
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
id: "sep-1",
|
id: "sep-1",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
|
id: "DTARegularAll",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
icons: {
|
||||||
|
16: "/style/button-regular.png",
|
||||||
|
32: "/style/button-regular@2x.png",
|
||||||
|
},
|
||||||
|
title: _("dta-regular-all"),
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
|
id: "DTATurboAll",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
icons: {
|
||||||
|
16: "/style/button-turbo.png",
|
||||||
|
32: "/style/button-turbo@2x.png",
|
||||||
|
},
|
||||||
|
title: _("dta-turbo-all"),
|
||||||
|
});
|
||||||
|
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
|
||||||
|
["all", "tools_menu"] :
|
||||||
|
["all", "browser_action", "tools_menu"];
|
||||||
|
mcreate({
|
||||||
|
id: "sep-2",
|
||||||
|
contexts: sep2ctx,
|
||||||
|
type: "separator"
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
|
id: "DTAAdd",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
icons: {
|
||||||
|
16: "/style/add.svg",
|
||||||
|
32: "/style/add.svg",
|
||||||
|
64: "/style/add.svg",
|
||||||
|
128: "/style/add.svg",
|
||||||
|
},
|
||||||
|
title: _("add-download"),
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
|
id: "sep-3",
|
||||||
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
|
type: "separator"
|
||||||
|
});
|
||||||
|
mcreate({
|
||||||
id: "DTAManager",
|
id: "DTAManager",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -203,7 +327,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("manager.short"),
|
title: _("manager.short"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTAPrefs",
|
id: "DTAPrefs",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -214,6 +338,29 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("prefs.short"),
|
title: _("prefs.short"),
|
||||||
});
|
});
|
||||||
|
Object.freeze(alls);
|
||||||
|
|
||||||
|
const adjustMenus = (v: boolean) => {
|
||||||
|
for (const [id, contexts] of alls.entries()) {
|
||||||
|
const adjusted = v ?
|
||||||
|
contexts.filter(e => e !== "all") :
|
||||||
|
contexts;
|
||||||
|
menus.update(id, {
|
||||||
|
contexts: adjusted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Prefs.get("hide-context", false).then((v: boolean) => {
|
||||||
|
// This is the initial load, so no need to adjust when visible already
|
||||||
|
if (!v) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adjustMenus(v);
|
||||||
|
});
|
||||||
|
Prefs.on("hide-context", (prefs, key, value: boolean) => {
|
||||||
|
adjustMenus(value);
|
||||||
|
});
|
||||||
|
|
||||||
menus.onClicked.addListener(this.onClicked);
|
menus.onClicked.addListener(this.onClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +382,7 @@ new class Menus extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findSingleItem(tab: any, url: string, turbo = false) {
|
async findSingleItem(tab: Tab, url: string, turbo = false) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -255,7 +402,7 @@ new class Menus extends Handler {
|
|||||||
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked(info: any, tab: any) {
|
onClicked(info: MenuClickInfo, tab: Tab) {
|
||||||
if (!tab.id) {
|
if (!tab.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -265,84 +412,123 @@ new class Menus extends Handler {
|
|||||||
console.error("Invalid Handler for", menuItemId);
|
console.error("Invalid Handler for", menuItemId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handler.call(this, info, tab).catch(console.error);
|
const rv: Promise<void> | void = handler.call(this, info, tab);
|
||||||
}
|
if (rv && rv.catch) {
|
||||||
|
rv.catch(console.error);
|
||||||
async onClickedDTARegularInternal(
|
|
||||||
selectionOnly: boolean, info: any, tab: any) {
|
|
||||||
try {
|
|
||||||
await this.processResults(
|
|
||||||
false,
|
|
||||||
await runContentJob(
|
|
||||||
tab, "/bundles/content-gather.js", {
|
|
||||||
type: "DTA:gather",
|
|
||||||
selectionOnly,
|
|
||||||
textLinks: await Prefs.get("text-links", true),
|
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegular(info: any, tab: any) {
|
async enumulate(action: string) {
|
||||||
return await this.onClickedDTARegularInternal(false, info, tab);
|
const tab = await tabs.query({
|
||||||
|
active: true,
|
||||||
|
currentWindow: true,
|
||||||
|
});
|
||||||
|
if (!tab || !tab.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onClicked({
|
||||||
|
menuItemId: action
|
||||||
|
}, tab[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularSelection(info: any, tab: any) {
|
async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTARegularInternal(true, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboInternal(selectionOnly: boolean, info: any, tab: any) {
|
async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
|
||||||
try {
|
return await this.performSelection({
|
||||||
await this.processResults(
|
selectionOnly: false,
|
||||||
true,
|
allTabs: true,
|
||||||
await runContentJob(
|
turbo: false,
|
||||||
tab, "/bundles/content-gather.js", {
|
tab,
|
||||||
type: "DTA:gather",
|
});
|
||||||
selectionOnly,
|
|
||||||
textLinks: await Prefs.get("text-links", true),
|
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurbo(info: any, tab: any) {
|
async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: true,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboSelection(info: any, tab: any) {
|
async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularLink(info: any, tab: any) {
|
async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.linkUrl, false);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: true,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboLink(info: any, tab: any) {
|
async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.linkUrl, true);
|
return await this.performSelection({
|
||||||
|
selectionOnly: true,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularImage(info: any, tab: any) {
|
async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
if (!info.linkUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.linkUrl, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboImage(info: any, tab: any) {
|
async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
if (!info.linkUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.linkUrl, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularMedia(info: any, tab: any) {
|
async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboMedia(info: any, tab: any) {
|
async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegularMedia(info: MenuClickInfo, tab: Tab) {
|
||||||
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTATurboMedia(info: MenuClickInfo, tab: Tab) {
|
||||||
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickedDTAAdd() {
|
||||||
|
API.singleRegular(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTAManager() {
|
async onClickedDTAManager() {
|
||||||
@ -352,12 +538,82 @@ new class Menus extends Handler {
|
|||||||
async onClickedDTAPrefs() {
|
async onClickedDTAPrefs() {
|
||||||
await openPrefs();
|
await openPrefs();
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
|
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"));
|
||||||
|
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 ? "" : "/windows/popup.html"
|
||||||
|
});
|
||||||
|
action.setIcon({
|
||||||
|
path: globalTurbo ? {
|
||||||
|
16: "/style/button-turbo.png",
|
||||||
|
32: "/style/button-turbo@2x.png",
|
||||||
|
} : {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function init() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
(async function init() {
|
|
||||||
await Prefs.set("last-run", new Date());
|
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);
|
||||||
|
});
|
||||||
await filters();
|
await filters();
|
||||||
await getManager();
|
await getManager();
|
||||||
})().catch(ex => {
|
})().catch(ex => {
|
||||||
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -98,9 +98,9 @@ export class BatchGenerator implements Generator {
|
|||||||
|
|
||||||
public readonly hasInvalid: boolean;
|
public readonly hasInvalid: boolean;
|
||||||
|
|
||||||
public readonly length: any;
|
public readonly length: number;
|
||||||
|
|
||||||
public readonly preview: any;
|
public readonly preview: string;
|
||||||
|
|
||||||
constructor(str: string) {
|
constructor(str: string) {
|
||||||
this.gens = [];
|
this.gens = [];
|
||||||
|
@ -3,15 +3,55 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const polyfill = require("webextension-polyfill");
|
const polyfill = require("webextension-polyfill");
|
||||||
|
|
||||||
export const {i18n} = polyfill;
|
interface ExtensionListener {
|
||||||
export const {extension} = polyfill;
|
addListener: (listener: Function) => void;
|
||||||
export const {notifications} = polyfill;
|
removeListener: (listener: Function) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageSender {
|
||||||
|
tab?: Tab;
|
||||||
|
frameId?: number;
|
||||||
|
id?: number;
|
||||||
|
url?: string;
|
||||||
|
tlsChannelId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuClickInfo {
|
||||||
|
menuItemId: string | number;
|
||||||
|
button?: number;
|
||||||
|
linkUrl?: string;
|
||||||
|
srcUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface RawPort {
|
||||||
|
error: any;
|
||||||
|
name: string;
|
||||||
|
onDisconnect: ExtensionListener;
|
||||||
|
onMessage: ExtensionListener;
|
||||||
|
sender?: MessageSender;
|
||||||
|
disconnect: () => void;
|
||||||
|
postMessage: (message: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const {browserAction} = polyfill;
|
export const {browserAction} = polyfill;
|
||||||
export const {contextMenus} = polyfill;
|
export const {contextMenus} = polyfill;
|
||||||
export const {downloads} = polyfill;
|
export const {downloads} = polyfill;
|
||||||
|
export const {extension} = polyfill;
|
||||||
|
export const {history} = polyfill;
|
||||||
export const {menus} = polyfill;
|
export const {menus} = polyfill;
|
||||||
|
export const {notifications} = polyfill;
|
||||||
export const {runtime} = polyfill;
|
export const {runtime} = polyfill;
|
||||||
|
export const {sessions} = polyfill;
|
||||||
export const {storage} = polyfill;
|
export const {storage} = polyfill;
|
||||||
export const {tabs} = polyfill;
|
export const {tabs} = polyfill;
|
||||||
export const {webNavigation} = polyfill;
|
export const {webNavigation} = polyfill;
|
||||||
|
export const {webRequest} = polyfill;
|
||||||
export const {windows} = polyfill;
|
export const {windows} = polyfill;
|
||||||
|
|
||||||
|
export const CHROME = navigator.appVersion.includes("Chrome/");
|
||||||
|
34
lib/bus.ts
@ -2,22 +2,18 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import {runtime, tabs} from "./browser";
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import {runtime, tabs, RawPort, MessageSender} from "./browser";
|
||||||
|
|
||||||
export class Port extends EventEmitter {
|
export class Port extends EventEmitter {
|
||||||
private port: any;
|
private port: RawPort | null;
|
||||||
|
|
||||||
constructor(port: any) {
|
constructor(port: RawPort) {
|
||||||
super();
|
super();
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
let disconnected = false;
|
let disconnected = false;
|
||||||
let tabListener: any;
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
if (tabListener) {
|
|
||||||
tabs.onRemoved.removeListener(tabListener);
|
|
||||||
tabListener = null;
|
|
||||||
}
|
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,11 +37,17 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
|
if (!this.port) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return this.port.name;
|
return this.port.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this.port.sender && this.port.sender.extensionId;
|
if (!this.port || !this.port.sender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.port.sender.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSelf() {
|
get isSelf() {
|
||||||
@ -53,6 +55,9 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post(msg: string, ...data: any[]) {
|
post(msg: string, ...data: any[]) {
|
||||||
|
if (!this.port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
this.port.postMessage({msg});
|
this.port.postMessage({msg});
|
||||||
return;
|
return;
|
||||||
@ -64,14 +69,17 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMessage(message: any) {
|
onMessage(message: any) {
|
||||||
if (Object.keys(message).includes("msg")) {
|
if (!this.port) {
|
||||||
this.emit(message.msg, message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(message)) {
|
if (Array.isArray(message)) {
|
||||||
message.forEach(this.onMessage, this);
|
message.forEach(this.onMessage, this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(message).includes("msg")) {
|
||||||
|
this.emit(message.msg, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
this.emit(message);
|
this.emit(message);
|
||||||
return;
|
return;
|
||||||
@ -99,7 +107,7 @@ export const Bus = new class extends EventEmitter {
|
|||||||
runtime.onConnect.addListener(this.onConnect.bind(this));
|
runtime.onConnect.addListener(this.onConnect.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(msg: any, sender: any, callback: any) {
|
onMessage(msg: any, sender: MessageSender, callback: any) {
|
||||||
let {type = null} = msg;
|
let {type = null} = msg;
|
||||||
if (!type) {
|
if (!type) {
|
||||||
type = msg;
|
type = msg;
|
||||||
@ -107,7 +115,7 @@ export const Bus = new class extends EventEmitter {
|
|||||||
this.emit(type, msg, callback);
|
this.emit(type, msg, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect(port: any) {
|
onConnect(port: RawPort) {
|
||||||
if (!port.name) {
|
if (!port.name) {
|
||||||
port.disconnect();
|
port.disconnect();
|
||||||
return;
|
return;
|
||||||
|
230
lib/cdheaderparser.ts
Normal 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(/^[^']*'/, ""));
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
const VERSION = 1;
|
const VERSION = 1;
|
||||||
@ -40,12 +43,12 @@ export const DB = new class DB {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllInternal(resolve: (items: any[]) => void, reject: Function) {
|
getAllInternal(resolve: (items: BaseItem[]) => void, reject: Function) {
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
reject(new Error("db closed"));
|
reject(new Error("db closed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const items: any[] = [];
|
const items: BaseItem[] = [];
|
||||||
const transaction = this.db.transaction(STORE, "readonly");
|
const transaction = this.db.transaction(STORE, "readonly");
|
||||||
transaction.onerror = ex => reject(ex);
|
transaction.onerror = ex => reject(ex);
|
||||||
const store = transaction.objectStore(STORE);
|
const store = transaction.objectStore(STORE);
|
||||||
|
106
lib/filters.ts
@ -4,13 +4,16 @@
|
|||||||
import uuid from "./uuid";
|
import uuid from "./uuid";
|
||||||
|
|
||||||
import "./objectoverlay";
|
import "./objectoverlay";
|
||||||
import { storage, i18n } from "./browser";
|
import { storage } from "./browser";
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import { Prefs } from "./prefs";
|
|
||||||
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Overlayable } from "./objectoverlay";
|
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
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
const REG_ESCAPE = /[{}()[\]\\^$.]/g;
|
const REG_ESCAPE = /[{}()[\]\\^$.]/g;
|
||||||
const REG_FNMATCH = /[*?]/;
|
const REG_FNMATCH = /[*?]/;
|
||||||
@ -91,7 +94,7 @@ function *parseIntoRegexpInternal(str: string): Iterable<RegExp> {
|
|||||||
|
|
||||||
// multi-expression
|
// multi-expression
|
||||||
if (str.includes(",")) {
|
if (str.includes(",")) {
|
||||||
for (const part in str.split(",")) {
|
for (const part of str.split(",")) {
|
||||||
yield *parseIntoRegexpInternal(part);
|
yield *parseIntoRegexpInternal(part);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -173,25 +176,37 @@ export class Matcher {
|
|||||||
}
|
}
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
matchItem(item: any) {
|
matchItem(item: BaseItem) {
|
||||||
const {usable = "", title = "", description = "", fileName = ""} = item;
|
const {usable = "", title = "", description = "", fileName = ""} = item;
|
||||||
return this.match(usable) || this.match(title) ||
|
return this.match(usable) || this.match(title) ||
|
||||||
this.match(description) || this.match(fileName);
|
this.match(description) || this.match(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RawFilter extends Object {
|
||||||
|
active: boolean;
|
||||||
|
type: number;
|
||||||
|
label: string;
|
||||||
|
expr: string;
|
||||||
|
icon?: string;
|
||||||
|
custom?: boolean;
|
||||||
|
isOverridden?: (prop: string) => boolean;
|
||||||
|
reset?: () => void;
|
||||||
|
toJSON?: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
export class Filter {
|
export class Filter {
|
||||||
private readonly owner: Filters;
|
private readonly owner: Filters;
|
||||||
|
|
||||||
public readonly id: any;
|
public readonly id: string | symbol;
|
||||||
|
|
||||||
private readonly raw: any;
|
private readonly raw: RawFilter;
|
||||||
|
|
||||||
private _label: string;
|
private _label: string;
|
||||||
|
|
||||||
private _reg: Matcher;
|
private _reg: Matcher;
|
||||||
|
|
||||||
constructor(owner: Filters, id: any, raw: any) {
|
constructor(owner: Filters, id: string | symbol, raw: RawFilter) {
|
||||||
if (!owner || !id || !raw) {
|
if (!owner || !id || !raw) {
|
||||||
throw new Error("null argument");
|
throw new Error("null argument");
|
||||||
}
|
}
|
||||||
@ -203,9 +218,11 @@ export class Filter {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._label = this.raw.label;
|
this._label = this.raw.label;
|
||||||
if (this.id !== FAST && this.id.startsWith("deffilter-") &&
|
if (typeof this.raw.isOverridden !== "undefined" &&
|
||||||
!this.raw.isOverridden("label")) {
|
typeof this.id === "string") {
|
||||||
this._label = i18n.getMessage(this.id) || this._label;
|
if (this.id.startsWith("deffilter-") && !this.raw.isOverridden("label")) {
|
||||||
|
this._label = _(this.id) || this._label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._reg = Matcher.fromExpression(this.expr);
|
this._reg = Matcher.fromExpression(this.expr);
|
||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
@ -281,7 +298,7 @@ export class Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reset() {
|
async reset() {
|
||||||
if (this.raw.custom) {
|
if (!this.raw.reset) {
|
||||||
throw Error("Cannot reset non-default filter");
|
throw Error("Cannot reset non-default filter");
|
||||||
}
|
}
|
||||||
this.raw.reset();
|
this.raw.reset();
|
||||||
@ -291,7 +308,10 @@ export class Filter {
|
|||||||
|
|
||||||
async "delete"() {
|
async "delete"() {
|
||||||
if (!this.raw.custom) {
|
if (!this.raw.custom) {
|
||||||
throw Error("Cannot delete default filter");
|
throw new Error("Cannot delete default filter");
|
||||||
|
}
|
||||||
|
if (typeof this.id !== "string") {
|
||||||
|
throw new Error("Cannot delete symbolized");
|
||||||
}
|
}
|
||||||
await this.owner.delete(this.id);
|
await this.owner.delete(this.id);
|
||||||
}
|
}
|
||||||
@ -300,7 +320,7 @@ export class Filter {
|
|||||||
return this._reg.match(str);
|
return this._reg.match(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
matchItem(item: any) {
|
matchItem(item: BaseItem) {
|
||||||
return this._reg.matchItem(item);
|
return this._reg.matchItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,8 +335,7 @@ class FastFilter extends Filter {
|
|||||||
throw new Error("Invalid fast filter value");
|
throw new Error("Invalid fast filter value");
|
||||||
}
|
}
|
||||||
super(owner, FAST, {
|
super(owner, FAST, {
|
||||||
id: FAST,
|
label: "fast",
|
||||||
label: FAST,
|
|
||||||
type: TYPE_ALL,
|
type: TYPE_ALL,
|
||||||
active: true,
|
active: true,
|
||||||
expr: value,
|
expr: value,
|
||||||
@ -351,8 +370,6 @@ class Filters extends EventEmitter {
|
|||||||
|
|
||||||
private filters: Filter[];
|
private filters: Filter[];
|
||||||
|
|
||||||
private fastFilter: string | null;
|
|
||||||
|
|
||||||
ignoreNext: boolean;
|
ignoreNext: boolean;
|
||||||
|
|
||||||
private readonly typeMatchers: Map<number, Matcher>;
|
private readonly typeMatchers: Map<number, Matcher>;
|
||||||
@ -362,10 +379,8 @@ class Filters extends EventEmitter {
|
|||||||
this.typeMatchers = new Map();
|
this.typeMatchers = new Map();
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.fastFilter = null;
|
|
||||||
this.ignoreNext = false;
|
this.ignoreNext = false;
|
||||||
this.regenerate();
|
this.regenerate();
|
||||||
Prefs.on("fast-filter", this.updateFastFilter.bind(this));
|
|
||||||
storage.onChanged.addListener(async (changes: any) => {
|
storage.onChanged.addListener(async (changes: any) => {
|
||||||
if (this.ignoreNext) {
|
if (this.ignoreNext) {
|
||||||
this.ignoreNext = false;
|
this.ignoreNext = false;
|
||||||
@ -403,6 +418,7 @@ class Filters extends EventEmitter {
|
|||||||
const id = `custom-${uuid()}`;
|
const id = `custom-${uuid()}`;
|
||||||
const filter = new Filter(this, id, {
|
const filter = new Filter(this, id, {
|
||||||
active: true,
|
active: true,
|
||||||
|
custom: true,
|
||||||
label,
|
label,
|
||||||
expr,
|
expr,
|
||||||
type,
|
type,
|
||||||
@ -411,11 +427,11 @@ class Filters extends EventEmitter {
|
|||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
"get"(id: any) {
|
"get"(id: string | symbol) {
|
||||||
return this.filters.find(e => e.id === id);
|
return this.filters.find(e => e.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async "delete"(id: any) {
|
async "delete"(id: string) {
|
||||||
const idx = this.filters.findIndex(e => e.id === id);
|
const idx = this.filters.findIndex(e => e.id === id);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return;
|
return;
|
||||||
@ -438,21 +454,12 @@ class Filters extends EventEmitter {
|
|||||||
return new FastFilter(this, value);
|
return new FastFilter(this, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFastFilter() {
|
async getFastFilter() {
|
||||||
if (!this.fastFilter) {
|
await FASTFILTER.init();
|
||||||
throw new Error("Nothing stored");
|
if (!FASTFILTER.current) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return new FastFilter(this, this.fastFilter);
|
return new FastFilter(this, FASTFILTER.current);
|
||||||
}
|
|
||||||
|
|
||||||
async setFastFilter(value: string) {
|
|
||||||
this.fastFilter = value || "";
|
|
||||||
await Prefs.set("fast-filter", this.fastFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFastFilter(pref: any, key: string, value: string) {
|
|
||||||
this.fastFilter = value || null;
|
|
||||||
this.regenerate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerate() {
|
regenerate() {
|
||||||
@ -480,17 +487,6 @@ class Filters extends EventEmitter {
|
|||||||
console.error("Filter", current.label || "unknown", ex);
|
console.error("Filter", current.label || "unknown", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.fastFilter) {
|
|
||||||
try {
|
|
||||||
const fastFilter = new FastFilter(this, this.fastFilter);
|
|
||||||
all.push(fastFilter);
|
|
||||||
links.push(fastFilter);
|
|
||||||
media.push(fastFilter);
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error("fast filter", this.fastFilter, "is invalid", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.typeMatchers.set(TYPE_ALL, new Matcher(all));
|
this.typeMatchers.set(TYPE_ALL, new Matcher(all));
|
||||||
this.typeMatchers.set(TYPE_LINK, new Matcher(links));
|
this.typeMatchers.set(TYPE_LINK, new Matcher(links));
|
||||||
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
|
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
|
||||||
@ -498,6 +494,7 @@ class Filters extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
await locale;
|
||||||
const defaultFilters = DEFAULT_FILTERS as any;
|
const defaultFilters = DEFAULT_FILTERS as any;
|
||||||
let savedFilters = (await storage.local.get("userFilters"));
|
let savedFilters = (await storage.local.get("userFilters"));
|
||||||
if (savedFilters && "userFilters" in savedFilters) {
|
if (savedFilters && "userFilters" in savedFilters) {
|
||||||
@ -534,14 +531,17 @@ class Filters extends EventEmitter {
|
|||||||
defaultFilters[filter]);
|
defaultFilters[filter]);
|
||||||
this.filters.push(new Filter(this, filter, current));
|
this.filters.push(new Filter(this, filter, current));
|
||||||
}
|
}
|
||||||
this.fastFilter = await Prefs.get("fast-filter", null);
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.regenerate();
|
this.regenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
filterItemsByType(items: any[], type: number) {
|
async filterItemsByType(items: BaseItem[], type: number) {
|
||||||
const matcher = this.typeMatchers.get(type);
|
const matcher = this.typeMatchers.get(type);
|
||||||
|
const fast = await this.getFastFilter();
|
||||||
return items.filter(function(item) {
|
return items.filter(function(item) {
|
||||||
|
if (fast && fast.matchItem(item)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return matcher && matcher.matchItem(item);
|
return matcher && matcher.matchItem(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -562,12 +562,14 @@ class Filters extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _filters: any;
|
let _filters: Filters;
|
||||||
|
let _loader: Promise<void>;
|
||||||
|
|
||||||
export async function filters(): Promise<Filters> {
|
export async function filters(): Promise<Filters> {
|
||||||
if (!_filters) {
|
if (!_loader) {
|
||||||
_filters = new Filters();
|
_filters = new Filters();
|
||||||
await _filters.load();
|
_loader = _filters.load();
|
||||||
}
|
}
|
||||||
|
await _loader;
|
||||||
return _filters;
|
return _filters;
|
||||||
}
|
}
|
||||||
|
298
lib/i18n.ts
@ -2,50 +2,239 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
|
import langs from "../_locales/all.json";
|
||||||
|
import { sorted, naturalCaseCompare } from "./sorting";
|
||||||
|
|
||||||
function load() {
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const CACHE_KEY = "_cached_locales";
|
||||||
|
const CUSTOM_KEY = "_custom_locale";
|
||||||
|
|
||||||
|
const normalizer = /[^A-Za-z0-9_]/g;
|
||||||
|
|
||||||
|
interface JSONEntry {
|
||||||
|
message: string;
|
||||||
|
placeholders: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entry {
|
||||||
|
private message: string;
|
||||||
|
|
||||||
|
constructor(entry: JSONEntry) {
|
||||||
|
if (!entry.message.includes("$")) {
|
||||||
|
throw new Error("Not entry-able");
|
||||||
|
}
|
||||||
|
let hit = false;
|
||||||
|
this.message = entry.message.replace(/\$[A-Z0-9]+\$/g, (r: string) => {
|
||||||
|
hit = true;
|
||||||
|
const id = r.substr(1, r.length - 2).toLocaleLowerCase();
|
||||||
|
const pholder = entry.placeholders[id];
|
||||||
|
if (!pholder || !pholder.content) {
|
||||||
|
throw new Error(`Invalid placeholder: ${id}`);
|
||||||
|
}
|
||||||
|
return `${pholder.content}$`;
|
||||||
|
});
|
||||||
|
if (!hit) {
|
||||||
|
throw new Error("Not entry-able");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localize(args: any[]) {
|
||||||
|
return this.message.replace(/\$\d+\$/g, (r: string) => {
|
||||||
|
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
|
||||||
|
return args[idx] || "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Localization {
|
||||||
|
private strings: Map<string, Entry | string>;
|
||||||
|
|
||||||
|
constructor(baseLanguage: any, ...overlayLanguages: any) {
|
||||||
|
this.strings = new Map();
|
||||||
|
const mapLanguage = (lang: any) => {
|
||||||
|
for (const [id, entry] of Object.entries<JSONEntry>(lang)) {
|
||||||
|
if (!entry.message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
if (entry.message.includes("$")) {
|
||||||
const {i18n} = require("webextension-polyfill");
|
this.strings.set(id, new Entry(entry));
|
||||||
|
}
|
||||||
return i18n;
|
else {
|
||||||
|
this.strings.set(id, entry.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
this.strings.set(id, entry.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mapLanguage(baseLanguage);
|
||||||
|
overlayLanguages.forEach(mapLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
localize(id: string, ...args: any[]) {
|
||||||
|
const entry = this.strings.get(id.replace(normalizer, "_"));
|
||||||
|
if (!entry) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
if (args.length === 1 && Array.isArray(args)) {
|
||||||
|
[args] = args;
|
||||||
|
}
|
||||||
|
return entry.localize(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBrowser() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
if (typeof browser !== "undefined" && browser.i18n) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof chrome !== "undefined" && chrome.i18n) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("not in a webext");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLanguage(code: string) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/_locales/${code}/messages.json`);
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadCached() {
|
||||||
|
if (document.location.pathname.includes("/windows/")) {
|
||||||
|
const cached = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached) as any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRawLocales() {
|
||||||
|
// 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 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 valid = Array.from(langs).filter(e => ALL_LANGS.has(e));
|
||||||
|
const fetched = await Promise.all(Array.from(valid, fetchLanguage));
|
||||||
|
return fetched.filter(e => !!e);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
valid = await loadRawLocales();
|
||||||
|
localStorage.setItem(CACHE_KEY, JSON.stringify(valid));
|
||||||
|
}
|
||||||
|
if (!valid.length) {
|
||||||
|
throw new Error("Could not lood ANY of these locales");
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom = localStorage.getItem(CUSTOM_KEY);
|
||||||
|
if (custom) {
|
||||||
|
try {
|
||||||
|
valid.push(JSON.parse(custom));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = valid.shift();
|
||||||
|
const rv = new Localization(base, ...valid);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to load locale", ex.toString(), ex.stack, ex);
|
||||||
|
return new Localization({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
// We might be running under node for tests
|
// We might be running under node for tests
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const messages = require("../_locales/en/messages.json");
|
const messages = require("../_locales/en/messages.json");
|
||||||
|
|
||||||
const map = new Map();
|
return new Localization(messages);
|
||||||
for (const [k, v] of Object.entries<any>(messages)) {
|
|
||||||
const {placeholders = {}} = v;
|
|
||||||
let {message = ""} = v;
|
|
||||||
for (const [pname, pval] of Object.entries<any>(placeholders)) {
|
|
||||||
message = message.replace(`$${pname.toUpperCase()}$`, `${pval.content}$`);
|
|
||||||
}
|
|
||||||
map.set(k, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getMessage(id: string, subst: string[]) {
|
|
||||||
const m = map.get(id);
|
|
||||||
if (typeof subst === undefined) {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(subst)) {
|
|
||||||
subst = [subst];
|
|
||||||
}
|
|
||||||
return m.replace(/\$\d+\$/g, (r: string) => {
|
|
||||||
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
|
|
||||||
return subst[idx] || "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = load();
|
type MemoLocalize = (id: string, ...args: any[]) => string;
|
||||||
const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
|
||||||
|
export const locale = load();
|
||||||
|
let loc: Localization | null;
|
||||||
|
let memoLocalize: MemoLocalize | null = null;
|
||||||
|
locale.then(l => {
|
||||||
|
loc = l;
|
||||||
|
memoLocalize = memoize(loc.localize.bind(loc), 10 * 1000, 10);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Localize a message
|
* Localize a message
|
||||||
@ -53,22 +242,22 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
|||||||
* @param {string[]} [subst] Message substituations
|
* @param {string[]} [subst] Message substituations
|
||||||
* @returns {string} Localized message
|
* @returns {string} Localized message
|
||||||
*/
|
*/
|
||||||
function _(id: string, ...subst: any[]) {
|
export function _(id: string, ...subst: any[]) {
|
||||||
|
if (!loc || !memoLocalize) {
|
||||||
|
console.trace("TOO SOON");
|
||||||
|
throw new Error("Called too soon");
|
||||||
|
}
|
||||||
if (!subst.length) {
|
if (!subst.length) {
|
||||||
return memoGetMessage(id);
|
return memoLocalize(id);
|
||||||
}
|
}
|
||||||
if (subst.length === 1 && Array.isArray(subst[0])) {
|
return loc.localize(id, subst);
|
||||||
subst = subst.pop();
|
|
||||||
}
|
|
||||||
return i18n.getMessage(id, subst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function localize_<T extends HTMLElement | DocumentFragment>(elem: T): T {
|
||||||
* Localize a DOM
|
for (const tmpl of elem.querySelectorAll<HTMLTemplateElement>("template")) {
|
||||||
* @param {Element} elem DOM to localize
|
localize_(tmpl.content);
|
||||||
* @returns {Element} Passed in element (fluent)
|
}
|
||||||
*/
|
|
||||||
function localize(elem: HTMLElement) {
|
|
||||||
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
|
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
|
||||||
const {i18n: i} = el.dataset;
|
const {i18n: i} = el.dataset;
|
||||||
if (!i) {
|
if (!i) {
|
||||||
@ -99,8 +288,25 @@ function localize(elem: HTMLElement) {
|
|||||||
for (const el of document.querySelectorAll("*[data-l18n]")) {
|
for (const el of document.querySelectorAll("*[data-l18n]")) {
|
||||||
console.error("wrong!", el);
|
console.error("wrong!", el);
|
||||||
}
|
}
|
||||||
return elem;
|
return elem as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localize a DOM
|
||||||
|
* @param {Element} elem DOM to localize
|
||||||
|
* @returns {Element} Passed in element (fluent)
|
||||||
|
*/
|
||||||
|
export async function localize<T extends HTMLElement | DocumentFragment>(
|
||||||
|
elem: T): Promise<T> {
|
||||||
|
await locale;
|
||||||
|
return localize_(elem);
|
||||||
|
}
|
||||||
|
|
||||||
export {localize, _};
|
export function saveCustomLocale(data?: string) {
|
||||||
|
if (!data) {
|
||||||
|
localStorage.removeItem(CUSTOM_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new Localization(JSON.parse(data));
|
||||||
|
localStorage.setItem(CUSTOM_KEY, data);
|
||||||
|
}
|
||||||
|
162
lib/iconcache.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}();
|
26
lib/item.ts
@ -4,18 +4,32 @@
|
|||||||
import { ALLOWED_SCHEMES } from "./constants";
|
import { ALLOWED_SCHEMES } from "./constants";
|
||||||
import { TRANSFERABLE_PROPERTIES } from "./constants";
|
import { TRANSFERABLE_PROPERTIES } from "./constants";
|
||||||
|
|
||||||
|
export interface BaseItem {
|
||||||
|
url: string;
|
||||||
|
usable: string;
|
||||||
|
referrer?: string;
|
||||||
|
usableReferrer?: string;
|
||||||
|
description?: string;
|
||||||
|
title?: string;
|
||||||
|
fileName?: string;
|
||||||
|
batch?: number;
|
||||||
|
idx: number;
|
||||||
|
mask?: string;
|
||||||
|
startDate?: number;
|
||||||
|
private?: boolean;
|
||||||
|
postData?: string;
|
||||||
|
paused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const OPTIONPROPS = Object.freeze([
|
const OPTIONPROPS = Object.freeze([
|
||||||
"referrer", "usableReferrer",
|
"referrer", "usableReferrer",
|
||||||
"description", "title",
|
"description", "title",
|
||||||
"fileName",
|
"fileName",
|
||||||
"batch", "idx",
|
"batch", "idx",
|
||||||
"mask",
|
"mask",
|
||||||
"fromMetalink",
|
|
||||||
"startDate",
|
"startDate",
|
||||||
"hashes",
|
|
||||||
"private",
|
"private",
|
||||||
"postData",
|
"postData",
|
||||||
"cleanRequest",
|
|
||||||
"paused"
|
"paused"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -34,7 +48,7 @@ function maybeAssign(options: any, what: any) {
|
|||||||
this[what] = val;
|
this[what] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Item {
|
export class Item implements BaseItem {
|
||||||
public url: string;
|
public url: string;
|
||||||
|
|
||||||
public usable: string;
|
public usable: string;
|
||||||
@ -43,6 +57,8 @@ export class Item {
|
|||||||
|
|
||||||
public usableReferrer: string;
|
public usableReferrer: string;
|
||||||
|
|
||||||
|
public idx: number;
|
||||||
|
|
||||||
constructor(raw: any, options?: any) {
|
constructor(raw: any, options?: any) {
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
|
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
|
||||||
@ -99,7 +115,7 @@ function transfer(e: any, other: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function makeUniqueItems(items: any, mapping?: Function) {
|
export function makeUniqueItems(items: any[][], mapping?: Function) {
|
||||||
const known = new Map();
|
const known = new Map();
|
||||||
const unique = [];
|
const unique = [];
|
||||||
for (const itemlist of items) {
|
for (const itemlist of items) {
|
||||||
|
@ -27,10 +27,11 @@ const SAVEDPROPS = [
|
|||||||
"written",
|
"written",
|
||||||
// server stuff
|
// server stuff
|
||||||
"serverName",
|
"serverName",
|
||||||
|
"browserName",
|
||||||
|
"mime",
|
||||||
|
"prerolled",
|
||||||
// other options
|
// other options
|
||||||
"private",
|
"private",
|
||||||
"fromMetalink",
|
|
||||||
"cleanRequest",
|
|
||||||
// db
|
// db
|
||||||
"manId",
|
"manId",
|
||||||
"dbId",
|
"dbId",
|
||||||
@ -41,10 +42,13 @@ const DEFAULTS = {
|
|||||||
state: QUEUED,
|
state: QUEUED,
|
||||||
error: "",
|
error: "",
|
||||||
serverName: "",
|
serverName: "",
|
||||||
|
browserName: "",
|
||||||
fileName: "",
|
fileName: "",
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
written: 0,
|
written: 0,
|
||||||
manId: 0,
|
manId: 0,
|
||||||
|
mime: "",
|
||||||
|
prerolled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
let sessionId = 0;
|
let sessionId = 0;
|
||||||
@ -61,14 +65,26 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public url: string;
|
public url: string;
|
||||||
|
|
||||||
|
public usable: string;
|
||||||
|
|
||||||
public uReferrer: URLd;
|
public uReferrer: URLd;
|
||||||
|
|
||||||
public referrer: string;
|
public referrer: string;
|
||||||
|
|
||||||
|
public usableReferrer: string;
|
||||||
|
|
||||||
public startDate: Date;
|
public startDate: Date;
|
||||||
|
|
||||||
public fileName: string;
|
public fileName: string;
|
||||||
|
|
||||||
|
public description?: string;
|
||||||
|
|
||||||
|
public title?: string;
|
||||||
|
|
||||||
|
public batch: number;
|
||||||
|
|
||||||
|
public idx: number;
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public postData: any;
|
public postData: any;
|
||||||
@ -81,8 +97,13 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public serverName: string;
|
public serverName: string;
|
||||||
|
|
||||||
|
public browserName: string;
|
||||||
|
|
||||||
|
public mime: string;
|
||||||
|
|
||||||
public mask: string;
|
public mask: string;
|
||||||
|
|
||||||
|
public prerolled: boolean;
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
Object.assign(this, DEFAULTS);
|
Object.assign(this, DEFAULTS);
|
||||||
@ -117,6 +138,10 @@ export class BaseDownload {
|
|||||||
return this.serverName || this.fileName || this.urlName || "index.html";
|
return this.serverName || this.fileName || this.urlName || "index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentName() {
|
||||||
|
return this.browserName || this.dest.name || this.finalName;
|
||||||
|
}
|
||||||
|
|
||||||
get urlName() {
|
get urlName() {
|
||||||
const path = parsePath(this.uURL);
|
const path = parsePath(this.uURL);
|
||||||
if (path.name) {
|
if (path.name) {
|
||||||
@ -154,7 +179,9 @@ export class BaseDownload {
|
|||||||
rv.destName = dest.name;
|
rv.destName = dest.name;
|
||||||
rv.destPath = dest.path;
|
rv.destPath = dest.path;
|
||||||
rv.destFull = dest.full;
|
rv.destFull = dest.full;
|
||||||
|
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||||
rv.error = this.error;
|
rv.error = this.error;
|
||||||
|
rv.ext = this.renamer.p_ext;
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,38 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
|
import { CHROME, downloads } from "../browser";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs } from "../prefs";
|
||||||
import { parsePath } from "../util";
|
|
||||||
import {
|
|
||||||
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
|
||||||
FORCABLE, PAUSABLE, CANCELABLE,
|
|
||||||
} from "./state";
|
|
||||||
import { BaseDownload } from "./basedownload";
|
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
|
import { filterInSitu, parsePath } from "../util";
|
||||||
|
import { BaseDownload } from "./basedownload";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Manager } from "./man";
|
import { Manager } from "./man";
|
||||||
import { downloads } from "../browser";
|
import Renamer from "./renamer";
|
||||||
|
import {
|
||||||
|
CANCELABLE,
|
||||||
|
CANCELED,
|
||||||
|
DONE,
|
||||||
|
FORCABLE,
|
||||||
|
MISSING,
|
||||||
|
PAUSABLE,
|
||||||
|
PAUSED,
|
||||||
|
QUEUED,
|
||||||
|
RUNNING
|
||||||
|
} from "./state";
|
||||||
|
import { Preroller } from "./preroller";
|
||||||
|
|
||||||
|
type Header = {name: string; value: string};
|
||||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
interface Options {
|
||||||
// ignored
|
conflictAction: string;
|
||||||
};
|
filename: string;
|
||||||
|
saveAs: boolean;
|
||||||
|
url: string;
|
||||||
|
method?: string;
|
||||||
|
body?: string;
|
||||||
|
incognito?: boolean;
|
||||||
|
headers: Header[];
|
||||||
|
}
|
||||||
|
|
||||||
export class Download extends BaseDownload {
|
export class Download extends BaseDownload {
|
||||||
public manager: Manager;
|
public manager: Manager;
|
||||||
@ -38,6 +54,7 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markDirty() {
|
markDirty() {
|
||||||
|
this.renamer = new Renamer(this);
|
||||||
this.manager.setDirty(this);
|
this.manager.setDirty(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +82,11 @@ export class Download extends BaseDownload {
|
|||||||
this.updateStateFromBrowser();
|
this.updateStateFromBrowser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state[0].state === "complete") {
|
||||||
|
this.changeState(DONE);
|
||||||
|
this.updateStateFromBrowser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!state[0].canResume) {
|
if (!state[0].canResume) {
|
||||||
throw new Error("Cannot resume");
|
throw new Error("Cannot resume");
|
||||||
}
|
}
|
||||||
@ -82,46 +104,58 @@ export class Download extends BaseDownload {
|
|||||||
if (this.state !== QUEUED) {
|
if (this.state !== QUEUED) {
|
||||||
throw new Error("invalid state");
|
throw new Error("invalid state");
|
||||||
}
|
}
|
||||||
console.trace("starting", this.toString(), this.dest, this.mask);
|
console.log("starting", this.toString(), this.toMsg());
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
|
|
||||||
|
// Do NOT await
|
||||||
|
this.reallyStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reallyStart() {
|
||||||
try {
|
try {
|
||||||
const options: any = {
|
if (!this.prerolled) {
|
||||||
|
await this.maybePreroll();
|
||||||
|
if (this.state !== RUNNING) {
|
||||||
|
// Aborted by preroll
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const options: Options = {
|
||||||
conflictAction: await Prefs.get("conflict-action"),
|
conflictAction: await Prefs.get("conflict-action"),
|
||||||
filename: this.dest.full,
|
filename: this.dest.full,
|
||||||
saveAs: false,
|
saveAs: false,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
headers: [{
|
headers: [],
|
||||||
name: "X-DTA-Tag",
|
|
||||||
value: this.sessionId.toString(),
|
|
||||||
}],
|
|
||||||
};
|
};
|
||||||
|
if (!CHROME && this.private) {
|
||||||
|
options.incognito = true;
|
||||||
|
}
|
||||||
if (this.postData) {
|
if (this.postData) {
|
||||||
options.body = this.postData;
|
options.body = this.postData;
|
||||||
options.method = "POST";
|
options.method = "POST";
|
||||||
}
|
}
|
||||||
if (this.private) {
|
if (!CHROME && this.referrer) {
|
||||||
options.incognito = true;
|
|
||||||
}
|
|
||||||
/* XXX "forbidden"
|
|
||||||
Cannot be worked around with webRequest either
|
|
||||||
as those do not see downloads.
|
|
||||||
if (this.referrer) {
|
|
||||||
options.headers.push({
|
options.headers.push({
|
||||||
name: "Referer",
|
name: "Referer",
|
||||||
value: this.referrer
|
value: this.referrer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
}
|
}
|
||||||
setShelfEnabled(false);
|
|
||||||
try {
|
try {
|
||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
}
|
}
|
||||||
finally {
|
catch (ex) {
|
||||||
setShelfEnabled(true);
|
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();
|
this.markDirty();
|
||||||
}
|
}
|
||||||
@ -132,6 +166,42 @@ 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.cancel();
|
||||||
|
this.error = 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) {
|
resume(forced = false) {
|
||||||
if (!(FORCABLE & this.state)) {
|
if (!(FORCABLE & this.state)) {
|
||||||
return;
|
return;
|
||||||
@ -161,9 +231,10 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
this.prerolled = false;
|
||||||
this.manId = 0;
|
this.manId = 0;
|
||||||
this.written = this.totalSize = 0;
|
this.written = this.totalSize = 0;
|
||||||
this.serverName = "";
|
this.mime = this.serverName = this.browserName = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromBrowser() {
|
async removeFromBrowser() {
|
||||||
@ -240,8 +311,11 @@ export class Download extends BaseDownload {
|
|||||||
const state = (await downloads.search({id: this.manId})).pop();
|
const state = (await downloads.search({id: this.manId})).pop();
|
||||||
const {filename, error} = state;
|
const {filename, error} = state;
|
||||||
const path = parsePath(filename);
|
const path = parsePath(filename);
|
||||||
this.serverName = path.name;
|
this.browserName = path.name;
|
||||||
this.adoptSize(state);
|
this.adoptSize(state);
|
||||||
|
if (!this.mime && state.mime) {
|
||||||
|
this.mime = state.mime;
|
||||||
|
}
|
||||||
this.markDirty();
|
this.markDirty();
|
||||||
switch (state.state) {
|
switch (state.state) {
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
|
@ -12,22 +12,27 @@ import { Prefs } from "../prefs";
|
|||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
import {Download} from "./download";
|
import { Download } from "./download";
|
||||||
import {ManagerPort} from "./port";
|
import { ManagerPort } from "./port";
|
||||||
import {Scheduler} from "./scheduler";
|
import { Scheduler } from "./scheduler";
|
||||||
import {Limits} from "./limits";
|
import { Limits } from "./limits";
|
||||||
import { downloads } from "../browser";
|
import { downloads, runtime } from "../browser";
|
||||||
|
|
||||||
|
|
||||||
const AUTOSAVE_TIMEOUT = 2000;
|
const AUTOSAVE_TIMEOUT = 2000;
|
||||||
const DIRTY_TIMEOUT = 100;
|
const DIRTY_TIMEOUT = 100;
|
||||||
// eslint-disable-next-line no-magic-numbers
|
// eslint-disable-next-line no-magic-numbers
|
||||||
const MISSING_TIMEOUT = 12 * 1000;
|
const MISSING_TIMEOUT = 12 * 1000;
|
||||||
|
const RELOAD_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||||
|
// ignored
|
||||||
|
};
|
||||||
|
|
||||||
export class Manager extends EventEmitter {
|
export class Manager extends EventEmitter {
|
||||||
private items: Download[];
|
private items: Download[];
|
||||||
|
|
||||||
private active: boolean;
|
public active: boolean;
|
||||||
|
|
||||||
private notifiedFinished: boolean;
|
private notifiedFinished: boolean;
|
||||||
|
|
||||||
@ -45,9 +50,12 @@ export class Manager extends EventEmitter {
|
|||||||
|
|
||||||
private scheduler: Scheduler | null;
|
private scheduler: Scheduler | null;
|
||||||
|
|
||||||
|
private shouldReload: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
this.shouldReload = false;
|
||||||
this.notifiedFinished = true;
|
this.notifiedFinished = true;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.saveQueue = new CoalescedUpdate(
|
this.saveQueue = new CoalescedUpdate(
|
||||||
@ -88,9 +96,19 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.items.push(rv);
|
this.items.push(rv);
|
||||||
});
|
});
|
||||||
await this.resetScheduler();
|
|
||||||
|
// Do not wait for the scheduler
|
||||||
|
this.resetScheduler();
|
||||||
|
|
||||||
this.emit("inited");
|
this.emit("inited");
|
||||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||||
|
runtime.onUpdateAvailable.addListener(() => {
|
||||||
|
if (this.running.size) {
|
||||||
|
this.shouldReload = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtime.reload();
|
||||||
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +154,7 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const next = await this.scheduler.next(this.running);
|
const next = await this.scheduler.next(this.running);
|
||||||
if (!next) {
|
if (!next) {
|
||||||
this.maybeNotifyFinished();
|
this.maybeRunFinishActions();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.running.has(next) || next.state !== QUEUED) {
|
if (this.running.has(next) || next.state !== QUEUED) {
|
||||||
@ -147,6 +165,7 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
next.changeState(CANCELED);
|
next.changeState(CANCELED);
|
||||||
|
next.error = ex.toString();
|
||||||
console.error(ex.toString(), ex);
|
console.error(ex.toString(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,10 +174,31 @@ export class Manager extends EventEmitter {
|
|||||||
async startDownload(download: Download) {
|
async startDownload(download: Download) {
|
||||||
// Add to running first, so we don't confuse the scheduler and other parts
|
// Add to running first, so we don't confuse the scheduler and other parts
|
||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
|
setShelfEnabled(false);
|
||||||
await download.start();
|
await download.start();
|
||||||
this.notifiedFinished = false;
|
this.notifiedFinished = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async maybeRunFinishActions() {
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.maybeNotifyFinished();
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.shouldReload) {
|
||||||
|
this.saveQueue.trigger();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtime.reload();
|
||||||
|
}, RELOAD_TIMEOUT);
|
||||||
|
}
|
||||||
|
setShelfEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
async maybeNotifyFinished() {
|
async maybeNotifyFinished() {
|
||||||
if (!(await Prefs.get("finish-notification"))) {
|
if (!(await Prefs.get("finish-notification"))) {
|
||||||
return;
|
return;
|
||||||
@ -215,7 +255,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.emit("dirty", items);
|
this.emit("dirty", items);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(items: Download[]) {
|
private save(items: Download[]) {
|
||||||
DB.saveItems(items.filter(i => !i.removed)).
|
DB.saveItems(items.filter(i => !i.removed)).
|
||||||
catch(console.error);
|
catch(console.error);
|
||||||
}
|
}
|
||||||
@ -265,7 +305,6 @@ export class Manager extends EventEmitter {
|
|||||||
changedState(download: Download, oldState: number, newState: number) {
|
changedState(download: Download, oldState: number, newState: number) {
|
||||||
if (oldState === RUNNING) {
|
if (oldState === RUNNING) {
|
||||||
this.running.delete(download);
|
this.running.delete(download);
|
||||||
this.maybeNotifyFinished();
|
|
||||||
}
|
}
|
||||||
if (newState === QUEUED) {
|
if (newState === QUEUED) {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
@ -278,7 +317,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.startNext();
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,6 +380,10 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit("active", this.active);
|
this.emit("active", this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMsgItems() {
|
||||||
|
return this.items.map(e => e.toMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inited: Promise<Manager>;
|
let inited: Promise<Manager>;
|
||||||
|
@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
|
|||||||
import { API } from "../api";
|
import { API } from "../api";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseDownload } from "./basedownload";
|
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 SID = {sid: number};
|
||||||
type SIDS = {
|
type SIDS = {
|
||||||
@ -13,9 +17,9 @@ type SIDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ManagerPort {
|
export class ManagerPort {
|
||||||
private manager: any;
|
private manager: Manager;
|
||||||
|
|
||||||
private port: any;
|
private port: Port;
|
||||||
|
|
||||||
constructor(manager: any, port: any) {
|
constructor(manager: any, port: any) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -61,6 +65,7 @@ export class ManagerPort {
|
|||||||
delete this.manager;
|
delete this.manager;
|
||||||
delete this.port;
|
delete this.port;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.port.post("active", this.manager.active);
|
this.port.post("active", this.manager.active);
|
||||||
this.sendAll();
|
this.sendAll();
|
||||||
}
|
}
|
||||||
@ -78,7 +83,6 @@ export class ManagerPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendAll() {
|
sendAll() {
|
||||||
this.port.post(
|
this.port.post("all", this.manager.getMsgItems());
|
||||||
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
220
lib/manager/preroller.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"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>();
|
||||||
|
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} = this.download;
|
||||||
|
const res = await fetch(uURL.toString(), {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Range: "bytes=0-1",
|
||||||
|
}),
|
||||||
|
mode: "same-origin",
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (res.body) {
|
||||||
|
res.body.cancel();
|
||||||
|
}
|
||||||
|
controller.abort();
|
||||||
|
const {headers} = res;
|
||||||
|
return this.finalize(headers, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prerollChrome() {
|
||||||
|
let rid = "";
|
||||||
|
const {uURL} = 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",
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
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 (status === 400 || status === 405 || status === 416) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { parsePath, sanitizePath } from "../util";
|
|
||||||
import { _ } from "../i18n";
|
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;
|
const REPLACE_EXPR = /\*\w+\*/gi;
|
||||||
|
|
||||||
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class Renamer {
|
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;
|
this.d = download;
|
||||||
|
const info = parsePath(this.d.finalName);
|
||||||
|
this.nameinfo = this.fixupExtension(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
get nameinfo() {
|
private fixupExtension(info: PathInfo): PathInfo {
|
||||||
return parsePath(this.d.finalName);
|
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() {
|
get ref() {
|
||||||
return this.d.uReferrer;
|
return this.d.uReferrer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get p_name() {
|
get p_name() {
|
||||||
return this.nameinfo.base;
|
return this.nameinfo.base;
|
||||||
}
|
}
|
||||||
@ -184,7 +208,7 @@ export default class Renamer {
|
|||||||
(self[prop] || "").trim() :
|
(self[prop] || "").trim() :
|
||||||
type;
|
type;
|
||||||
if (flat) {
|
if (flat) {
|
||||||
return rv.replace(/\/+/g, "-");
|
return rv.replace(/[/\\]+/g, "-");
|
||||||
}
|
}
|
||||||
return rv;
|
return rv;
|
||||||
}));
|
}));
|
||||||
|
65
lib/mime.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}();
|
@ -12,13 +12,20 @@ const DEFAULTS = {
|
|||||||
message: "message",
|
message: "message",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TIMEOUT = 4000;
|
||||||
|
|
||||||
|
let gid = 1;
|
||||||
|
|
||||||
export class Notification extends EventEmitter {
|
export class Notification extends EventEmitter {
|
||||||
private notification: any;
|
private notification: any;
|
||||||
|
|
||||||
|
private readonly generated: boolean;
|
||||||
|
|
||||||
constructor(id: string | null, options = {}) {
|
constructor(id: string | null, options = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
id = id || "DownThemAll-notification";
|
this.generated = !id;
|
||||||
|
id = id || `DownThemAll-notification${++gid}`;
|
||||||
if (typeof options === "string") {
|
if (typeof options === "string") {
|
||||||
options = {message: options};
|
options = {message: options};
|
||||||
}
|
}
|
||||||
@ -39,11 +46,16 @@ export class Notification extends EventEmitter {
|
|||||||
opened(notification: any) {
|
opened(notification: any) {
|
||||||
this.notification = notification;
|
this.notification = notification;
|
||||||
this.emit("opened", this);
|
this.emit("opened", this);
|
||||||
|
if (this.generated) {
|
||||||
|
setTimeout(() => {
|
||||||
|
notifications.clear(notification);
|
||||||
|
}, TIMEOUT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clicked(notification: any, button?: number) {
|
clicked(notification: any, button?: number) {
|
||||||
// We can only be clicked, when we were opened, at which point the
|
// 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) {
|
if (notification !== this.notification) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,6 +64,7 @@ export class Notification extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.emit("clicked", this);
|
this.emit("clicked", this);
|
||||||
|
console.log("clicked", notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
async closed(notification: any) {
|
async closed(notification: any) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as DEFAULT_PREFS from "../data/prefs.json";
|
import DEFAULT_PREFS from "../data/prefs.json";
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import {loadOverlay} from "./objectoverlay";
|
import { loadOverlay } from "./objectoverlay";
|
||||||
import { storage } from "./browser";
|
import { storage } from "./browser";
|
||||||
|
|
||||||
const PREFS = Symbol("PREFS");
|
const PREFS = Symbol("PREFS");
|
||||||
|
@ -36,7 +36,7 @@ export class RecentList {
|
|||||||
this.pref = `savedlist-${pref}`;
|
this.pref = `savedlist-${pref}`;
|
||||||
this.defaults = Array.from(defaults);
|
this.defaults = Array.from(defaults);
|
||||||
this[LIST] = [];
|
this[LIST] = [];
|
||||||
this.limit = 5;
|
this.limit = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
@ -102,8 +102,8 @@ export const MASK = new RecentList("mask", [
|
|||||||
"*name*.*ext*",
|
"*name*.*ext*",
|
||||||
"*num*_*name*.*ext*",
|
"*num*_*name*.*ext*",
|
||||||
"*url*-*name*.*ext*",
|
"*url*-*name*.*ext*",
|
||||||
"*name* (*text*).*ext*",
|
"downthemall/*y*-*m*/*name*.*ext*",
|
||||||
"*name* (*hh*-*mm*).*ext*"
|
"*name* (*text*).*ext*"
|
||||||
]);
|
]);
|
||||||
MASK.init().catch(console.error);
|
MASK.init().catch(console.error);
|
||||||
|
|
||||||
|
@ -10,10 +10,24 @@ import { donate, openPrefs, openUrls } from "./windowutils";
|
|||||||
import { filters, FAST, Filter } from "./filters";
|
import { filters, FAST, Filter } from "./filters";
|
||||||
import { WindowStateTracker } from "./windowstatetracker";
|
import { WindowStateTracker } from "./windowstatetracker";
|
||||||
import { windows } from "./browser";
|
import { windows } from "./browser";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
|
interface BaseMatchedItem extends BaseItem {
|
||||||
|
matched?: string | null;
|
||||||
|
prevMatched?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
export interface ItemDelta {
|
||||||
let ws = items.map((item: any, idx: number) => {
|
idx: number;
|
||||||
|
matched?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSelection(
|
||||||
|
filters: Filter[],
|
||||||
|
items: BaseMatchedItem[],
|
||||||
|
onlyFast: boolean): ItemDelta[] {
|
||||||
|
let ws = items.map((item, idx: number) => {
|
||||||
item.idx = idx;
|
item.idx = idx;
|
||||||
const {matched = null} = item;
|
const {matched = null} = item;
|
||||||
item.prevMatched = matched;
|
item.prevMatched = matched;
|
||||||
@ -23,14 +37,20 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
|||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
ws = ws.filter(item => {
|
ws = ws.filter(item => {
|
||||||
if (filter.matchItem(item)) {
|
if (filter.matchItem(item)) {
|
||||||
item.matched = filter.id === FAST ?
|
if (filter.id === FAST) {
|
||||||
"fast" :
|
item.matched = "fast";
|
||||||
(onlyFast ? null : filter.id);
|
}
|
||||||
|
else if (!onlyFast && typeof filter.id === "string") {
|
||||||
|
item.matched = filter.id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item.matched = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return !item.matched;
|
return !item.matched;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items.filter(item => item.prevMatched !== item.matched). map(item => {
|
return items.filter(item => item.prevMatched !== item.matched).map(item => {
|
||||||
return {
|
return {
|
||||||
idx: item.idx,
|
idx: item.idx,
|
||||||
matched: item.matched
|
matched: item.matched
|
||||||
@ -41,6 +61,9 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
|||||||
function *computeActiveFiltersGen(
|
function *computeActiveFiltersGen(
|
||||||
filters: Filter[], activeOverrides: Map<string, boolean>) {
|
filters: Filter[], activeOverrides: Map<string, boolean>) {
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
|
if (typeof filter.id !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const override = activeOverrides.get(filter.id);
|
const override = activeOverrides.get(filter.id);
|
||||||
if (typeof override === "boolean") {
|
if (typeof override === "boolean") {
|
||||||
if (override) {
|
if (override) {
|
||||||
@ -59,11 +82,11 @@ function computeActiveFilters(
|
|||||||
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
|
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
function filtersToDescs(filters: any[]) {
|
function filtersToDescs(filters: Filter[]) {
|
||||||
return filters.map(f => f.descriptor);
|
return filters.map(f => f.descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function select(links: any[], media: any[]) {
|
export async function select(links: BaseItem[], media: BaseItem[]) {
|
||||||
const fm = await filters();
|
const fm = await filters();
|
||||||
const tracker = new WindowStateTracker("select", {
|
const tracker = new WindowStateTracker("select", {
|
||||||
minWidth: 700,
|
minWidth: 700,
|
||||||
@ -75,6 +98,7 @@ export async function select(links: any[], media: any[]) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port = await Promise.race<Port>([
|
const port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
||||||
@ -85,26 +109,26 @@ export async function select(links: any[], media: any[]) {
|
|||||||
tracker.track(window.id, port);
|
tracker.track(window.id, port);
|
||||||
|
|
||||||
const overrides = new Map();
|
const overrides = new Map();
|
||||||
let fast: any = null;
|
let fast: Filter | null = null;
|
||||||
let onlyFast: false;
|
let onlyFast: false;
|
||||||
try {
|
try {
|
||||||
fast = fm.getFastFilter();
|
fast = await fm.getFastFilter();
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendFilters = function(delta = false) {
|
const sendFilters = function(delta = false) {
|
||||||
let {linkFilters, mediaFilters} = fm;
|
const {linkFilters, mediaFilters} = fm;
|
||||||
const alink = computeActiveFilters(linkFilters, overrides);
|
const alink = computeActiveFilters(linkFilters, overrides);
|
||||||
const amedia = computeActiveFilters(mediaFilters, overrides);
|
const amedia = computeActiveFilters(mediaFilters, overrides);
|
||||||
const sactiveFilters = new Set<any>();
|
const sactiveFilters = new Set<any>();
|
||||||
[alink, amedia].forEach(
|
[alink, amedia].forEach(
|
||||||
a => a.forEach(filter => sactiveFilters.add(filter.id)));
|
a => a.forEach(filter => sactiveFilters.add(filter.id)));
|
||||||
const activeFilters = Array.from(sactiveFilters);
|
const activeFilters = Array.from(sactiveFilters);
|
||||||
linkFilters = filtersToDescs(linkFilters);
|
const linkFilterDescs = filtersToDescs(linkFilters);
|
||||||
mediaFilters = filtersToDescs(mediaFilters);
|
const mediaFilterDescs = filtersToDescs(mediaFilters);
|
||||||
port.post("filters", {linkFilters, mediaFilters, activeFilters});
|
port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
|
||||||
|
|
||||||
if (fast) {
|
if (fast) {
|
||||||
alink.unshift(fast);
|
alink.unshift(fast);
|
||||||
@ -128,9 +152,6 @@ export async function select(links: any[], media: any[]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
port.on("queue", (msg: any) => {
|
port.on("queue", (msg: any) => {
|
||||||
const selected = new Set<number>(msg.items);
|
|
||||||
const items = (msg.type === "links" ? links : media);
|
|
||||||
msg.items = items.filter((item: any, idx: number) => selected.has(idx));
|
|
||||||
done.resolve(msg);
|
done.resolve(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,7 +196,11 @@ export async function select(links: any[], media: any[]) {
|
|||||||
sendFilters(false);
|
sendFilters(false);
|
||||||
const type = await Prefs.get("last-type", "links");
|
const type = await Prefs.get("last-type", "links");
|
||||||
port.post("items", {type, links, media});
|
port.post("items", {type, links, media});
|
||||||
const result = await done;
|
const {items, options} = await done;
|
||||||
|
const selectedIndexes = new Set<number>(items);
|
||||||
|
const selectedList = (options.type === "links" ? links : media);
|
||||||
|
const selectedItems = selectedList.filter(
|
||||||
|
(item: BaseItem, idx: number) => selectedIndexes.has(idx));
|
||||||
for (const [filter, override] of overrides) {
|
for (const [filter, override] of overrides) {
|
||||||
const f = fm.get(filter);
|
const f = fm.get(filter);
|
||||||
if (f) {
|
if (f) {
|
||||||
@ -183,7 +208,7 @@ export async function select(links: any[], media: any[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await fm.save();
|
await fm.save();
|
||||||
return result;
|
return {items: selectedItems, options};
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
fm.off("changed", sendFilters);
|
fm.off("changed", sendFilters);
|
||||||
|
@ -7,8 +7,10 @@ import { WindowStateTracker } from "./windowstatetracker";
|
|||||||
import { Promised, timeout } from "./util";
|
import { Promised, timeout } from "./util";
|
||||||
import { donate } from "./windowutils";
|
import { donate } from "./windowutils";
|
||||||
import { windows } from "./browser";
|
import { windows } from "./browser";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
export async function single(item: any) {
|
export async function single(item: BaseItem | null) {
|
||||||
const tracker = new WindowStateTracker("single", {
|
const tracker = new WindowStateTracker("single", {
|
||||||
minWidth: 700,
|
minWidth: 700,
|
||||||
minHeight: 460
|
minHeight: 460
|
||||||
@ -19,6 +21,7 @@ export async function single(item: any) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port: Port = await Promise.race<Port>([
|
const port: Port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
||||||
@ -46,7 +49,9 @@ export async function single(item: any) {
|
|||||||
donate();
|
donate();
|
||||||
});
|
});
|
||||||
|
|
||||||
port.post("item", item);
|
if (item) {
|
||||||
|
port.post("item", {item});
|
||||||
|
}
|
||||||
return await done;
|
return await done;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
78
lib/util.ts
@ -2,8 +2,8 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as psl from "psl";
|
import * as psl from "psl";
|
||||||
import {memoize, identity} from "./memoize";
|
import { identity, memoize } from "./memoize";
|
||||||
export {debounce} from "../uikit/lib/util";
|
export { debounce } from "../uikit/lib/util";
|
||||||
|
|
||||||
export class Promised {
|
export class Promised {
|
||||||
private promise: Promise<any>;
|
private promise: Promise<any>;
|
||||||
@ -96,8 +96,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
|
|||||||
export const sanitizePath = identity(
|
export const sanitizePath = identity(
|
||||||
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
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
|
// 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) {
|
if (path instanceof URL) {
|
||||||
path = decodeURIComponent(path.pathname);
|
path = decodeURIComponent(path.pathname);
|
||||||
}
|
}
|
||||||
@ -127,13 +191,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path = pieces.join("/");
|
path = pieces.join("/");
|
||||||
return {
|
return new PathInfo(base, ext, path);
|
||||||
path,
|
|
||||||
name,
|
|
||||||
base,
|
|
||||||
ext,
|
|
||||||
full: path ? `${path}/${name}` : name
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class CoalescedUpdate<T> extends Set<T> {
|
export class CoalescedUpdate<T> extends Set<T> {
|
||||||
|
@ -3,17 +3,31 @@
|
|||||||
|
|
||||||
import { windows, tabs, runtime } from "../lib/browser";
|
import { windows, tabs, runtime } from "../lib/browser";
|
||||||
import {getManager} from "./manager/man";
|
import {getManager} from "./manager/man";
|
||||||
import * as DEFAULT_ICONS from "../data/icons.json";
|
import DEFAULT_ICONS from "../data/icons.json";
|
||||||
|
|
||||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||||
const MANAGER_URL = "/windows/manager.html";
|
const MANAGER_URL = "/windows/manager.html";
|
||||||
|
|
||||||
const IS_CHROME = navigator && navigator.userAgent.includes("Chrome");
|
|
||||||
|
|
||||||
|
|
||||||
export async function mostRecentBrowser(): Promise<any> {
|
export async function mostRecentBrowser(): Promise<any> {
|
||||||
let window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
let window;
|
||||||
|
try {
|
||||||
|
window = await windows.getCurrent({windowTypes: ["normal"]});
|
||||||
|
if (window.type !== "normal") {
|
||||||
|
throw new Error("not a normal window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
try {
|
||||||
|
window = await windows.getlastFocused({windowTypes: ["normal"]});
|
||||||
|
if (window.type !== "normal") {
|
||||||
|
throw new Error("not a normal window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||||
filter((w: any) => w.type === "normal").pop();
|
filter((w: any) => w.type === "normal").pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!window) {
|
if (!window) {
|
||||||
window = await windows.create({
|
window = await windows.create({
|
||||||
url: DONATE_URL,
|
url: DONATE_URL,
|
||||||
@ -106,32 +120,10 @@ const ICONS = Object.freeze((() => {
|
|||||||
return new Map<string, string>(rv);
|
return new Map<string, string>(rv);
|
||||||
})());
|
})());
|
||||||
|
|
||||||
let iconForPathPlatform: Function;
|
export const DEFAULT_ICON_SIZE = 16;
|
||||||
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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
// eslint-disable-next-line no-magic-numbers
|
export function iconForPath(path: string, size = DEFAULT_ICON_SIZE) {
|
||||||
export function iconForPath(path: string, size = 16) {
|
|
||||||
const web = /^https?:\/\//.test(path);
|
const web = /^https?:\/\//.test(path);
|
||||||
let file = path.split(/[\\/]/).pop();
|
let file = path.split(/[\\/]/).pop();
|
||||||
if (file) {
|
if (file) {
|
||||||
@ -152,7 +144,7 @@ export function iconForPath(path: string, size = 16) {
|
|||||||
file = "file";
|
file = "file";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return iconForPathPlatform(file, size);
|
return ICONS.get(file) || "icon-file-generic";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DownThemAll!",
|
"name": "DownThemAll!",
|
||||||
"version": "4.0.1",
|
"version": "4.0.10",
|
||||||
|
|
||||||
"description": "__MSG_extensionDescription__",
|
"description": "__MSG_extensionDescription__",
|
||||||
"homepage_url": "https://downthemall.org/",
|
"homepage_url": "https://downthemall.org/",
|
||||||
@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
"default_locale": "en",
|
"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": {
|
"icons": {
|
||||||
"16": "style/icon16.png",
|
"16": "style/icon16.png",
|
||||||
"32": "style/icon32.png",
|
"32": "style/icon32.png",
|
||||||
@ -22,14 +24,17 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"menus",
|
|
||||||
"downloads",
|
"downloads",
|
||||||
"downloads.open",
|
"downloads.open",
|
||||||
"downloads.shelf",
|
"downloads.shelf",
|
||||||
|
"history",
|
||||||
|
"menus",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webNavigation"
|
"webNavigation",
|
||||||
|
"webRequest"
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
@ -40,7 +45,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": false,
|
"browser_style": true,
|
||||||
|
"default_popup": "windows/popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"16": "style/icon16.png",
|
"16": "style/icon16.png",
|
||||||
"32": "style/icon32.png",
|
"32": "style/icon32.png",
|
||||||
|
10
package.json
@ -22,18 +22,20 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||||
"@typescript-eslint/parser": "^2.0.0",
|
"@typescript-eslint/parser": "^2.0.0",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"eslint": "^6.1.0",
|
"eslint": "^6.2.2",
|
||||||
"mocha": "^6.2.0",
|
"mocha": "^6.2.0",
|
||||||
"ts-loader": "^6.0.4",
|
"ts-loader": "^6.0.4",
|
||||||
"ts-node": "^8.3.0",
|
"ts-node": "^8.3.0",
|
||||||
"typescript": "^3.5.3",
|
"typescript": "^3.5.3",
|
||||||
"webpack": "^4.39.2",
|
"webpack": "^4.39.3",
|
||||||
"webpack-cli": "^3.3.6",
|
"webpack-cli": "^3.3.7",
|
||||||
"xregexp": "^4.2.4"
|
"xregexp": "^4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/psl": "^1.1.0",
|
"@types/psl": "^1.1.0",
|
||||||
|
"@types/whatwg-mimetype": "^2.1.0",
|
||||||
"psl": "^1.3.0",
|
"psl": "^1.3.0",
|
||||||
"webextension-polyfill": "^0.4.0"
|
"webextension-polyfill": "^0.4.0",
|
||||||
|
"whatwg-mimetype": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,9 @@ return url;
|
|||||||
}();
|
}();
|
||||||
|
|
||||||
function makeURL(url: string) {
|
function makeURL(url: string) {
|
||||||
return new URL(url, baseURL);
|
const rv = new URL(url, baseURL);
|
||||||
|
rv.hash = "";
|
||||||
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitize(str: string | null | undefined) {
|
function sanitize(str: string | null | undefined) {
|
||||||
@ -92,12 +94,13 @@ class Gatherer {
|
|||||||
this.schemes = new Set(options.schemes);
|
this.schemes = new Set(options.schemes);
|
||||||
this.transferable = options.transferable;
|
this.transferable = options.transferable;
|
||||||
this.collectLink = this.collectLink.bind(this);
|
this.collectLink = this.collectLink.bind(this);
|
||||||
this.collectImages = this.collectImages.bind(this);
|
this.collectImage = this.collectImage.bind(this);
|
||||||
this.collectMedia = this.collectMedia.bind(this);
|
this.collectMedia = this.collectMedia.bind(this);
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
collectLink(a: HTMLAnchorElement) {
|
collectLink(a: HTMLAnchorElement) {
|
||||||
|
try {
|
||||||
const item = this.makeItem(a.href, a);
|
const item = this.makeItem(a.href, a);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return item;
|
return item;
|
||||||
@ -107,13 +110,21 @@ class Gatherer {
|
|||||||
item.description = extractDescription(a);
|
item.description = extractDescription(a);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("oopsed link", ex.toString(), ex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
*collectImagesInternal(img: HTMLImageElement) {
|
*collectImageInternal(img: HTMLImageElement) {
|
||||||
|
try {
|
||||||
const src = img.currentSrc || img.src;
|
const src = img.currentSrc || img.src;
|
||||||
const item = this.makeItem(src, img);
|
const item = this.makeItem(src, img);
|
||||||
|
if (item) {
|
||||||
item.fileName = "";
|
item.fileName = "";
|
||||||
item.description = item.title;
|
item.description = item.title;
|
||||||
yield item;
|
yield item;
|
||||||
|
}
|
||||||
|
|
||||||
const {srcset} = img;
|
const {srcset} = img;
|
||||||
if (!srcset) {
|
if (!srcset) {
|
||||||
@ -126,13 +137,19 @@ class Gatherer {
|
|||||||
for (const i of imgs) {
|
for (const i of imgs) {
|
||||||
const item = this.makeItem(i, img);
|
const item = this.makeItem(i, img);
|
||||||
if (item) {
|
if (item) {
|
||||||
|
item.fileName = "";
|
||||||
|
item.description = item.title;
|
||||||
yield item;
|
yield item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("oops image", ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
collectImages(img: HTMLImageElement) {
|
collectImage(img: HTMLImageElement) {
|
||||||
return [...this.collectImagesInternal(img)];
|
return [...this.collectImageInternal(img)];
|
||||||
}
|
}
|
||||||
|
|
||||||
collectMediaInternal(title: string | undefined | null, el: HTMLMediaElement) {
|
collectMediaInternal(title: string | undefined | null, el: HTMLMediaElement) {
|
||||||
@ -156,6 +173,7 @@ class Gatherer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collectMedia(el: HTMLMediaElement) {
|
collectMedia(el: HTMLMediaElement) {
|
||||||
|
try {
|
||||||
const item = this.collectMediaInternal(el.getAttribute("title"), el);
|
const item = this.collectMediaInternal(el.getAttribute("title"), el);
|
||||||
const rv = item ? [item] : [];
|
const rv = item ? [item] : [];
|
||||||
const title: string | undefined = item && item.title ||
|
const title: string | undefined = item && item.title ||
|
||||||
@ -164,6 +182,11 @@ class Gatherer {
|
|||||||
map(this.collectMediaInternal.bind(this, title)));
|
map(this.collectMediaInternal.bind(this, title)));
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("oopsed media", ex.toString(), ex);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
*findTexts() {
|
*findTexts() {
|
||||||
let doc = document;
|
let doc = document;
|
||||||
@ -207,9 +230,15 @@ class Gatherer {
|
|||||||
if (!this.textLinks) {
|
if (!this.textLinks) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
return Array.from(this.findTextLinks()).
|
return Array.from(this.findTextLinks()).
|
||||||
map(link => this.makeItem(link.href, link));
|
map(link => this.makeItem(link.href, link));
|
||||||
}
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("oopsed textlinks", ex.toString(), ex);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
makeItem(surl: string, el: HTMLElement, title?: string | null): any {
|
makeItem(surl: string, el: HTMLElement, title?: string | null): any {
|
||||||
if (!(el as any).fake && this.selectionOnly &&
|
if (!(el as any).fake && this.selectionOnly &&
|
||||||
@ -266,7 +295,7 @@ class Gatherer {
|
|||||||
function gather(msg: any, sender: any, callback: Function) {
|
function gather(msg: any, sender: any, callback: Function) {
|
||||||
try {
|
try {
|
||||||
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
const gatherer = new Gatherer(msg);
|
const gatherer = new Gatherer(msg);
|
||||||
const result = {
|
const result = {
|
||||||
@ -276,7 +305,7 @@ function gather(msg: any, sender: any, callback: Function) {
|
|||||||
gatherer.collectTextLinks()),
|
gatherer.collectTextLinks()),
|
||||||
media: gatherer.makeUniqueItems(
|
media: gatherer.makeUniqueItems(
|
||||||
Array.from(document.querySelectorAll("img")).
|
Array.from(document.querySelectorAll("img")).
|
||||||
flatMap(gatherer.collectImages),
|
flatMap(gatherer.collectImage),
|
||||||
Array.from(document.querySelectorAll("video")).
|
Array.from(document.querySelectorAll("video")).
|
||||||
flatMap(gatherer.collectMedia),
|
flatMap(gatherer.collectMedia),
|
||||||
Array.from(document.querySelectorAll("audio")).
|
Array.from(document.querySelectorAll("audio")).
|
||||||
@ -284,10 +313,11 @@ function gather(msg: any, sender: any, callback: Function) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
urlToUsable(result, result.baseURL);
|
urlToUsable(result, result.baseURL);
|
||||||
callback(result);
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex.toString(), ex.stack, ex);
|
console.error(ex.toString(), ex.stack, ex);
|
||||||
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
style/add.svg
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m8 0a8 8 0 0 0-8 8 8 8 0 0 0 8 8 8 8 0 0 0 8-8 8 8 0 0 0-8-8zm-1.5 3h3v3.5h3.5v3h-3.5v3.5h-3v-3.5h-3.5v-3h3.5v-3.5z" fill="#000080" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 312 B |
Before Width: | Height: | Size: 767 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 1.5 KiB |
@ -18,12 +18,17 @@
|
|||||||
--folder-color: rgb(214, 165, 4);
|
--folder-color: rgb(214, 165, 4);
|
||||||
--maskbutton-color: rgb(236, 185, 16);
|
--maskbutton-color: rgb(236, 185, 16);
|
||||||
--missing-color: rgb(0, 82, 204);
|
--missing-color: rgb(0, 82, 204);
|
||||||
|
--open-color: rgba(236, 185, 16, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-platform="mac"] {
|
html[data-platform="mac"] {
|
||||||
--folder-color: rgb(4, 102, 214);
|
--folder-color: rgb(4, 102, 214);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-size: 10pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'downthemall';
|
font-family: 'downthemall';
|
||||||
src: url('downthemall.woff2?75791791') format('woff2');
|
src: url('downthemall.woff2?75791791') format('woff2');
|
||||||
|
@ -46,6 +46,11 @@ body > * {
|
|||||||
background: rgb(246,246,246);
|
background: rgb(246,246,246);
|
||||||
color: black;
|
color: black;
|
||||||
transition: box-shadow 0.5s, background 1s;
|
transition: box-shadow 0.5s, background 1s;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
#toolbar > .button > span:before {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolbar > .button.disabled {
|
#toolbar > .button.disabled {
|
||||||
@ -63,13 +68,6 @@ body > * {
|
|||||||
box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75);
|
box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolbar > .button > span {
|
|
||||||
display: block;
|
|
||||||
flex-grow: 0;
|
|
||||||
width: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar > .button > .icon-add {
|
#toolbar > .button > .icon-add {
|
||||||
color: var(--add-color);
|
color: var(--add-color);
|
||||||
}
|
}
|
||||||
@ -110,11 +108,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colURL {
|
#colURL {
|
||||||
width: 38%;
|
width: 42%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colPercent {
|
#colPercent {
|
||||||
width: 3em;
|
width: 4em;
|
||||||
min-width: 3em;
|
min-width: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +121,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colSize {
|
#colSize {
|
||||||
width: 15em;
|
width: 14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colSpeed {
|
#colSpeed {
|
||||||
width: 6em;
|
width: 7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colDomain,
|
#colDomain,
|
||||||
@ -156,6 +154,14 @@ body > * {
|
|||||||
height: 26px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtualtable-row.opening {
|
||||||
|
background: var(--open-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-progress-container {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.virtualtable-progress-bar {
|
.virtualtable-progress-bar {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
@ -264,6 +270,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtualtable-column-6,
|
.virtualtable-column-6,
|
||||||
|
.virtualtable-column-4,
|
||||||
.virtualtable-column-3 {
|
.virtualtable-column-3 {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -432,6 +439,8 @@ body > * {
|
|||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 2px 2px 6px black;
|
box-shadow: 2px 2px 6px black;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tooltip-infos {
|
#tooltip-infos {
|
||||||
@ -444,8 +453,13 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tooltip-icon {
|
#tooltip-icon {
|
||||||
font-size: 48px;
|
height: 64px;
|
||||||
line-height: 48px;
|
width: 64px;
|
||||||
|
background-size: 64px 64px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
font-size: 64px;
|
||||||
|
line-height: 64px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
grid-row: 1/-1;
|
grid-row: 1/-1;
|
||||||
|
@ -19,7 +19,7 @@ article {
|
|||||||
|
|
||||||
#tabs {
|
#tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
||||||
padding-left: calc(2em + 32px);
|
padding-left: calc(2em + 32px);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ input.tab {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabs > label{
|
#tabs > label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
|
Before Width: | Height: | Size: 923 B |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 2.0 KiB |
@ -117,7 +117,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
||||||
#tabs {
|
#tabs {
|
||||||
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ body > * {
|
|||||||
background: var(--toolbar-bg-color);
|
background: var(--toolbar-bg-color);
|
||||||
color: white;
|
color: white;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
padding: 1.2ex;
|
padding: 1ex;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -232,3 +232,7 @@ body > * {
|
|||||||
#maskButton {
|
#maskButton {
|
||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@ -49,7 +49,7 @@ p.example {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#options > * {
|
#options > * {
|
||||||
margin: 0;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#options > input {
|
#options > input {
|
||||||
@ -81,3 +81,7 @@ h3 {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
289
tests/test_cdheaderparser.js
Normal 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
@ -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;
|
||||||
|
});
|
||||||
|
});
|
@ -19,7 +19,7 @@ const OPTS = {
|
|||||||
state: DownloadState.QUEUED,
|
state: DownloadState.QUEUED,
|
||||||
batch: 42,
|
batch: 42,
|
||||||
idx: 23,
|
idx: 23,
|
||||||
mask: "*name*.*ext",
|
mask: "*name*.*ext*",
|
||||||
description: "desc / ript.ion .",
|
description: "desc / ript.ion .",
|
||||||
title: " *** TITLE *** ",
|
title: " *** TITLE *** ",
|
||||||
};
|
};
|
||||||
@ -57,6 +57,49 @@ describe("Renamer", function() {
|
|||||||
expect(dest.path).to.equal("");
|
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() {
|
it("*text*", function() {
|
||||||
const dest = makeOne("*text*");
|
const dest = makeOne("*text*");
|
||||||
expect(dest.full).to.equal("desc/ript.ion");
|
expect(dest.full).to.equal("desc/ript.ion");
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"importHelpers": true,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: right;
|
justify-content: flex-end;
|
||||||
background: rgba(30, 30, 30, 0.2);
|
background: rgba(30, 30, 30, 0.2);
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
border-top: 1px solid rgba(30, 30, 30, 0.5);
|
border-top: 1px solid rgba(30, 30, 30, 0.5);
|
||||||
|
@ -14,6 +14,8 @@ import {
|
|||||||
} from "./tablesymbols";
|
} from "./tablesymbols";
|
||||||
import { InvalidatedSet, UpdateRecord } from "./tableutil";
|
import { InvalidatedSet, UpdateRecord } from "./tableutil";
|
||||||
import { addClass, clampUInt, IS_MAC } from "./util";
|
import { addClass, clampUInt, IS_MAC } from "./util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { TableConfig } from "./config";
|
||||||
|
|
||||||
const ROWS_SMALL_UPDATE = 5;
|
const ROWS_SMALL_UPDATE = 5;
|
||||||
const PIXEL_PREC = 5;
|
const PIXEL_PREC = 5;
|
||||||
@ -79,7 +81,7 @@ export class BaseTable extends AbstractTable {
|
|||||||
|
|
||||||
[COLS]: Columns;
|
[COLS]: Columns;
|
||||||
|
|
||||||
constructor(elem: any, config: any, version?: number) {
|
constructor(elem: any, config: TableConfig | null, version?: number) {
|
||||||
config = (config && config.version === version && config) || {};
|
config = (config && config.version === version && config) || {};
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -121,9 +123,9 @@ export class BaseTable extends AbstractTable {
|
|||||||
this.makeDOM(config);
|
this.makeDOM(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDOM(config: any) {
|
makeDOM(config: TableConfig) {
|
||||||
const configColumns = "columns" in config ? config.columns : null;
|
const configColumns = "columns" in config ? config.columns : null;
|
||||||
const cols = this[COLS] = new Columns(this, configColumns);
|
const cols = this[COLS] = new Columns(this, configColumns || null);
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
const thead = document.createElement("div");
|
const thead = document.createElement("div");
|
||||||
@ -241,7 +243,7 @@ export class BaseTable extends AbstractTable {
|
|||||||
return new SelectionRange(firstIdx, lastIdx);
|
return new SelectionRange(firstIdx, lastIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): TableConfig {
|
||||||
return {
|
return {
|
||||||
version: this.version,
|
version: this.version,
|
||||||
columns: this.columnConfig
|
columns: this.columnConfig
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import { TableEvents } from "./tableevents";
|
import { TableEvents } from "./tableevents";
|
||||||
import {addClass, debounce, sum} from "./util";
|
import {addClass, debounce, sum} from "./util";
|
||||||
import {EventEmitter} from "./events";
|
import {EventEmitter} from "./events";
|
||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
|
import { ColumnConfig, ColumnConfigs } from "./config";
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
// License: MIT
|
|
||||||
|
|
||||||
const PIXLIT_WIDTH = 2;
|
const PIXLIT_WIDTH = 2;
|
||||||
const MIN_COL_WIDTH = 16;
|
const MIN_COL_WIDTH = 16;
|
||||||
@ -55,8 +53,7 @@ export class Column extends EventEmitter {
|
|||||||
columns: Columns,
|
columns: Columns,
|
||||||
col: HTMLTableHeaderCellElement,
|
col: HTMLTableHeaderCellElement,
|
||||||
id: number,
|
id: number,
|
||||||
config: any) {
|
config: ColumnConfig | null) {
|
||||||
config = config || {};
|
|
||||||
super();
|
super();
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.elem = col;
|
this.elem = col;
|
||||||
@ -89,7 +86,7 @@ export class Column extends EventEmitter {
|
|||||||
|
|
||||||
this.elem.appendChild(containerElem);
|
this.elem.appendChild(containerElem);
|
||||||
|
|
||||||
if ("visible" in config) {
|
if (config) {
|
||||||
this.visible = config.visible;
|
this.visible = config.visible;
|
||||||
}
|
}
|
||||||
this.initWidths(config);
|
this.initWidths(config);
|
||||||
@ -148,18 +145,18 @@ export class Column extends EventEmitter {
|
|||||||
return Math.max(0, this.currentWidth - this.minWidth);
|
return Math.max(0, this.currentWidth - this.minWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): ColumnConfig {
|
||||||
return {
|
return {
|
||||||
visible: this.visible,
|
visible: this.visible,
|
||||||
width: this.currentWidth,
|
width: this.currentWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initWidths(config: any) {
|
initWidths(config: ColumnConfig | null) {
|
||||||
const style = getComputedStyle(this.elem, null);
|
const style = getComputedStyle(this.elem, null);
|
||||||
this.minWidth = toPixel(style.getPropertyValue("min-width"), MIN_COL_WIDTH);
|
this.minWidth = toPixel(style.getPropertyValue("min-width"), MIN_COL_WIDTH);
|
||||||
this.maxWidth = toPixel(style.getPropertyValue("max-width"), 0);
|
this.maxWidth = toPixel(style.getPropertyValue("max-width"), 0);
|
||||||
const width = config.width || this.baseWidth;
|
const width = (config && config.width) || this.baseWidth;
|
||||||
this.setWidth(width);
|
this.setWidth(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +233,7 @@ export class Columns extends EventEmitter {
|
|||||||
|
|
||||||
public visible: Column[];
|
public visible: Column[];
|
||||||
|
|
||||||
constructor(table: any, config: any) {
|
constructor(table: any, config: ColumnConfigs | null) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
super();
|
super();
|
||||||
this.table = table;
|
this.table = table;
|
||||||
@ -247,7 +244,9 @@ export class Columns extends EventEmitter {
|
|||||||
this.named = new Map<string, Column>();
|
this.named = new Map<string, Column>();
|
||||||
this.cols = Array.from(table.elem.querySelectorAll("th")).
|
this.cols = Array.from(table.elem.querySelectorAll("th")).
|
||||||
map((colEl: HTMLTableHeaderCellElement, colid: number) => {
|
map((colEl: HTMLTableHeaderCellElement, colid: number) => {
|
||||||
const columnConfig = colEl.id in config ? config[colEl.id] : null;
|
const columnConfig = config && colEl.id in config ?
|
||||||
|
config[colEl.id] :
|
||||||
|
null;
|
||||||
const col = new Column(this, colEl, colid, columnConfig);
|
const col = new Column(this, colEl, colid, columnConfig);
|
||||||
col.on("gripmoved", this.gripmoved);
|
col.on("gripmoved", this.gripmoved);
|
||||||
this.named.set(colEl.id, col);
|
this.named.set(colEl.id, col);
|
||||||
@ -261,7 +260,7 @@ export class Columns extends EventEmitter {
|
|||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): ColumnConfigs {
|
||||||
const rv: any = {};
|
const rv: any = {};
|
||||||
for (const c of this.cols) {
|
for (const c of this.cols) {
|
||||||
rv[c.elem.id] = c.config;
|
rv[c.elem.id] = c.config;
|
||||||
|
13
uikit/lib/config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
export interface ColumnConfig {
|
||||||
|
visible: boolean;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
export type ColumnConfigs ={ [name: string]: ColumnConfig };
|
||||||
|
|
||||||
|
export interface TableConfig {
|
||||||
|
version?: number;
|
||||||
|
columns?: ColumnConfigs;
|
||||||
|
}
|
@ -10,7 +10,7 @@ const MENU_OPEN_BOUNCE = 500;
|
|||||||
|
|
||||||
let ids = 0;
|
let ids = 0;
|
||||||
|
|
||||||
const Keys = new Map([
|
export const Keys = new Map([
|
||||||
["ACCEL", IS_MAC ? "⌘" : "Ctrl"],
|
["ACCEL", IS_MAC ? "⌘" : "Ctrl"],
|
||||||
["CTRL", "Ctrl"],
|
["CTRL", "Ctrl"],
|
||||||
["ALT", IS_MAC ? "⌥" : "Alt"],
|
["ALT", IS_MAC ? "⌥" : "Alt"],
|
||||||
@ -33,6 +33,14 @@ export interface MenuPosition {
|
|||||||
clientY: number;
|
clientY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MenuOptions {
|
||||||
|
disabled?: string;
|
||||||
|
allowClick?: string;
|
||||||
|
icon?: string;
|
||||||
|
key?: string;
|
||||||
|
autoHide?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class MenuItemBase {
|
export class MenuItemBase {
|
||||||
public readonly owner: ContextMenu;
|
public readonly owner: ContextMenu;
|
||||||
|
|
||||||
@ -44,7 +52,7 @@ export class MenuItemBase {
|
|||||||
|
|
||||||
public readonly key: string;
|
public readonly key: string;
|
||||||
|
|
||||||
public readonly autohide: boolean;
|
public readonly autoHide: boolean;
|
||||||
|
|
||||||
public readonly elem: HTMLLIElement;
|
public readonly elem: HTMLLIElement;
|
||||||
|
|
||||||
@ -54,18 +62,16 @@ export class MenuItemBase {
|
|||||||
|
|
||||||
public readonly keyElem: HTMLSpanElement;
|
public readonly keyElem: HTMLSpanElement;
|
||||||
|
|
||||||
constructor(owner: ContextMenu, id = "", text = "", {
|
constructor(owner: ContextMenu, id = "", text = "", options: MenuOptions) {
|
||||||
key = "", icon = "", autohide = Object()
|
|
||||||
}) {
|
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
id = `contextmenu-${++ids}`;
|
id = `contextmenu-${++ids}`;
|
||||||
}
|
}
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.text = text || "";
|
this.text = text || "";
|
||||||
this.icon = icon || "";
|
this.icon = options.icon || "";
|
||||||
this.key = key || "";
|
this.key = options.key || "";
|
||||||
this.autohide = autohide !== "false" && autohide !== false;
|
this.autoHide = options.autoHide !== "false";
|
||||||
|
|
||||||
this.elem = document.createElement("li");
|
this.elem = document.createElement("li");
|
||||||
this.elem.id = this.id;
|
this.elem.id = this.id;
|
||||||
@ -96,13 +102,14 @@ export class MenuItemBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MenuItem extends MenuItemBase {
|
export class MenuItem extends MenuItemBase {
|
||||||
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
|
constructor(
|
||||||
|
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
super(owner, id, text, options);
|
super(owner, id, text, options);
|
||||||
this.disabled = !!options.disabled;
|
this.disabled = options.disabled === "true";
|
||||||
this.elem.setAttribute("aria-role", "menuitem");
|
this.elem.setAttribute("aria-role", "menuitem");
|
||||||
this.elem.addEventListener(
|
this.elem.addEventListener(
|
||||||
"click", () => this.owner.emit("clicked", this.id, this.autohide));
|
"click", () => this.owner.emit("clicked", this.id, this.autoHide));
|
||||||
}
|
}
|
||||||
|
|
||||||
get disabled() {
|
get disabled() {
|
||||||
@ -132,7 +139,8 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
|
|
||||||
public readonly expandElem: HTMLSpanElement;
|
public readonly expandElem: HTMLSpanElement;
|
||||||
|
|
||||||
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
|
constructor(
|
||||||
|
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
|
||||||
super(owner, id, text, options);
|
super(owner, id, text, options);
|
||||||
this.elem.setAttribute("aria-role", "menuitem");
|
this.elem.setAttribute("aria-role", "menuitem");
|
||||||
this.elem.setAttribute("aria-haspopup", "true");
|
this.elem.setAttribute("aria-haspopup", "true");
|
||||||
@ -145,8 +153,8 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
this.expandElem.textContent = "►";
|
this.expandElem.textContent = "►";
|
||||||
this.elem.appendChild(this.expandElem);
|
this.elem.appendChild(this.expandElem);
|
||||||
this.elem.addEventListener("click", event => {
|
this.elem.addEventListener("click", event => {
|
||||||
if (options.allowClick) {
|
if (options.allowClick === "true") {
|
||||||
this.owner.emit("clicked", this.id, this.autohide);
|
this.owner.emit("clicked", this.id, this.autoHide);
|
||||||
}
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -160,7 +168,7 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
this.owner.on("showing", () => {
|
this.owner.on("showing", () => {
|
||||||
this.menu.dismiss();
|
this.menu.dismiss();
|
||||||
});
|
});
|
||||||
this.menu.on("clicked", (...args: any) => {
|
this.menu.on("clicked", (...args: any[]) => {
|
||||||
this.owner.emit("clicked", ...args);
|
this.owner.emit("clicked", ...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -215,7 +223,7 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
export class ContextMenu extends EventEmitter {
|
export class ContextMenu extends EventEmitter {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
items: any[];
|
items: MenuItemBase[];
|
||||||
|
|
||||||
itemMap: Map<string, MenuItemBase>;
|
itemMap: Map<string, MenuItemBase>;
|
||||||
|
|
||||||
@ -223,7 +231,7 @@ export class ContextMenu extends EventEmitter {
|
|||||||
|
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
|
||||||
_maybeDismiss: any;
|
_maybeDismiss: (this: Window, ev: MouseEvent) => any;
|
||||||
|
|
||||||
constructor(el?: any) {
|
constructor(el?: any) {
|
||||||
super();
|
super();
|
||||||
@ -348,10 +356,12 @@ export class ContextMenu extends EventEmitter {
|
|||||||
return this.itemMap.get(id);
|
return this.itemMap.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(item: MenuItemBase, before: any = "") {
|
add(item: MenuItemBase, before: MenuItemBase | string = "") {
|
||||||
let idx = this.items.length;
|
let idx = this.items.length;
|
||||||
if (before) {
|
if (before) {
|
||||||
before = before.id || before;
|
if (typeof before !== "string") {
|
||||||
|
before = before.id;
|
||||||
|
}
|
||||||
const ni = this.items.findIndex(i => i.id === before);
|
const ni = this.items.findIndex(i => i.id === before);
|
||||||
if (ni >= 0) {
|
if (ni >= 0) {
|
||||||
idx = ni;
|
idx = ni;
|
||||||
@ -366,8 +376,8 @@ export class ContextMenu extends EventEmitter {
|
|||||||
this.itemMap.set(item.id, item);
|
this.itemMap.set(item.id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(item: any) {
|
remove(item: MenuItemBase | string) {
|
||||||
const id = item.id || item;
|
const id = typeof item === "string" ? item : item.id;
|
||||||
const idx = this.items.findIndex(i => i.id === id);
|
const idx = this.items.findIndex(i => i.id === id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.items.splice(idx, 1);
|
this.items.splice(idx, 1);
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
interface ModalButton {
|
export interface ModalButton {
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
dismiss?: boolean;
|
dismiss?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ModalDialog {
|
interface Promised {
|
||||||
private _showing: any;
|
resolve: Function;
|
||||||
|
reject: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default abstract class ModalDialog {
|
||||||
|
private _showing: Promised | null;
|
||||||
|
|
||||||
private _dismiss: HTMLButtonElement | null;
|
private _dismiss: HTMLButtonElement | null;
|
||||||
|
|
||||||
@ -23,7 +28,7 @@ export default class ModalDialog {
|
|||||||
this._default = null;
|
this._default = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeEl() {
|
async _makeEl() {
|
||||||
this._dismiss = null;
|
this._dismiss = null;
|
||||||
this._default = null;
|
this._default = null;
|
||||||
|
|
||||||
@ -35,7 +40,7 @@ export default class ModalDialog {
|
|||||||
|
|
||||||
const body = document.createElement("article");
|
const body = document.createElement("article");
|
||||||
body.classList.add("modal-body");
|
body.classList.add("modal-body");
|
||||||
body.appendChild(this.content);
|
body.appendChild(await this.getContent());
|
||||||
cont.appendChild(body);
|
cont.appendChild(body);
|
||||||
|
|
||||||
const footer = document.createElement("footer");
|
const footer = document.createElement("footer");
|
||||||
@ -87,9 +92,8 @@ export default class ModalDialog {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content(): DocumentFragment | HTMLElement {
|
abstract getContent():
|
||||||
throw new Error("Not implemented");
|
Promise<DocumentFragment | HTMLElement> | HTMLElement | DocumentFragment;
|
||||||
}
|
|
||||||
|
|
||||||
get buttons(): ModalButton[] {
|
get buttons(): ModalButton[] {
|
||||||
return [
|
return [
|
||||||
@ -108,7 +112,10 @@ export default class ModalDialog {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
done(button: any) {
|
done(button: ModalButton) {
|
||||||
|
if (!this._showing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const value = this.convertValue(button.value);
|
const value = this.convertValue(button.value);
|
||||||
if (button.dismiss) {
|
if (button.dismiss) {
|
||||||
this._showing.reject(new Error(value));
|
this._showing.reject(new Error(value));
|
||||||
@ -131,7 +138,7 @@ export default class ModalDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async show() {
|
async show(): Promise<any> {
|
||||||
if (this._showing) {
|
if (this._showing) {
|
||||||
throw new Error("Double show");
|
throw new Error("Double show");
|
||||||
}
|
}
|
||||||
@ -160,7 +167,7 @@ export default class ModalDialog {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.appendChild(this.el = this._makeEl());
|
document.body.appendChild(this.el = await this._makeEl());
|
||||||
this.shown();
|
this.shown();
|
||||||
addEventListener("keydown", escapeHandler);
|
addEventListener("keydown", escapeHandler);
|
||||||
addEventListener("keydown", enterHandler);
|
addEventListener("keydown", enterHandler);
|
||||||
@ -205,7 +212,7 @@ export default class ModalDialog {
|
|||||||
*/
|
*/
|
||||||
static async inform(title: string, text: string, oktext: string) {
|
static async inform(title: string, text: string, oktext: string) {
|
||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Information";
|
h.textContent = title || "Information";
|
||||||
@ -241,7 +248,7 @@ export default class ModalDialog {
|
|||||||
|
|
||||||
static async confirm(title: string, text: string) {
|
static async confirm(title: string, text: string) {
|
||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Confirm";
|
h.textContent = title || "Confirm";
|
||||||
@ -280,7 +287,7 @@ export default class ModalDialog {
|
|||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
_input: HTMLInputElement;
|
_input: HTMLInputElement;
|
||||||
|
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Confirm";
|
h.textContent = title || "Confirm";
|
||||||
|
@ -38,7 +38,7 @@ class Hover {
|
|||||||
|
|
||||||
private hovering: boolean;
|
private hovering: boolean;
|
||||||
|
|
||||||
private timer: any;
|
private timer: number | null;
|
||||||
|
|
||||||
constructor(row: Row) {
|
constructor(row: Row) {
|
||||||
this.row = row;
|
this.row = row;
|
||||||
@ -62,7 +62,7 @@ class Hover {
|
|||||||
this.elem.addEventListener("mousemove", this.onmove, {passive: true});
|
this.elem.addEventListener("mousemove", this.onmove, {passive: true});
|
||||||
this.x = evt.clientX;
|
this.x = evt.clientX;
|
||||||
this.y = evt.clientY;
|
this.y = evt.clientY;
|
||||||
this.timer = setTimeout(this.onhover, HOVER_TIME);
|
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
onleave() {
|
onleave() {
|
||||||
@ -93,7 +93,7 @@ class Hover {
|
|||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
}
|
}
|
||||||
this.timer = setTimeout(this.onhover, HOVER_TIME);
|
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import {Row} from "./row";
|
|||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
import {COLS, ROWCACHE, VISIBLE} from "./tablesymbols";
|
import {COLS, ROWCACHE, VISIBLE} from "./tablesymbols";
|
||||||
import {ContextMenu, MenuItem} from "./contextmenu";
|
import {ContextMenu, MenuItem} from "./contextmenu";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { TableConfig } from "./config";
|
||||||
|
|
||||||
const RESIZE_DEBOUNCE = 500;
|
const RESIZE_DEBOUNCE = 500;
|
||||||
const SCROLL_DEBOUNCE = 250;
|
const SCROLL_DEBOUNCE = 250;
|
||||||
@ -19,7 +21,7 @@ const SCROLL_DEBOUNCE = 250;
|
|||||||
export class TableEvents extends BaseTable {
|
export class TableEvents extends BaseTable {
|
||||||
private oldVisibleTop: number;
|
private oldVisibleTop: number;
|
||||||
|
|
||||||
constructor(elem: any, config?: any, version?: number) {
|
constructor(elem: any, config: TableConfig | null, version?: number) {
|
||||||
super(elem, config, version);
|
super(elem, config, version);
|
||||||
const {selection} = this;
|
const {selection} = this;
|
||||||
selection.on("selection-added", this.selectionAdded.bind(this));
|
selection.on("selection-added", this.selectionAdded.bind(this));
|
||||||
@ -172,7 +174,7 @@ export class TableEvents extends BaseTable {
|
|||||||
ctx,
|
ctx,
|
||||||
id,
|
id,
|
||||||
col.spanElem.textContent || "",
|
col.spanElem.textContent || "",
|
||||||
{autohide: "false"});
|
{autoHide: "false"});
|
||||||
ctx.add(item);
|
ctx.add(item);
|
||||||
item.iconElem.textContent = col.visible ? "✓" : " ";
|
item.iconElem.textContent = col.visible ? "✓" : " ";
|
||||||
ctx.on(id, async () => {
|
ctx.on(id, async () => {
|
||||||
|
@ -8,6 +8,8 @@ import { Row } from "./row";
|
|||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
import {ROW_CACHE_SIZE, ROW_REUSE_SIZE} from "./constants";
|
import {ROW_CACHE_SIZE, ROW_REUSE_SIZE} from "./constants";
|
||||||
import {clampUInt} from "./util";
|
import {clampUInt} from "./util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseTable } from "./basetable";
|
||||||
|
|
||||||
|
|
||||||
export class InvalidatedSet<T> extends Set<T> {
|
export class InvalidatedSet<T> extends Set<T> {
|
||||||
@ -70,7 +72,7 @@ export class UpdateRecord {
|
|||||||
|
|
||||||
bottom: number;
|
bottom: number;
|
||||||
|
|
||||||
constructor(table: any, cols: Column[]) {
|
constructor(table: BaseTable, cols: Column[]) {
|
||||||
this.rowCount = table.rowCount;
|
this.rowCount = table.rowCount;
|
||||||
this.scrollTop = table.visibleTop;
|
this.scrollTop = table.visibleTop;
|
||||||
this.rowHeight = table.rowHeight;
|
this.rowHeight = table.rowHeight;
|
||||||
|
@ -13,14 +13,25 @@ export function addClass(elem: HTMLElement, ...cls: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(fn: Function, to: number) {
|
interface Timer {
|
||||||
let timer: any;
|
args: any[];
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(fn: Function, to: number, reset?: boolean) {
|
||||||
|
let timer: Timer | null;
|
||||||
return function(...args: any[]) {
|
return function(...args: any[]) {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
|
if (!reset) {
|
||||||
timer.args = args;
|
timer.args = args;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(function() {
|
window.clearTimeout(timer.id);
|
||||||
|
}
|
||||||
|
const id = window.setTimeout(function() {
|
||||||
|
if (!timer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const {args} = timer;
|
const {args} = timer;
|
||||||
timer = null;
|
timer = null;
|
||||||
try {
|
try {
|
||||||
@ -30,7 +41,7 @@ export function debounce(fn: Function, to: number) {
|
|||||||
console.error(ex.toString(), ex);
|
console.error(ex.toString(), ex);
|
||||||
}
|
}
|
||||||
}, to);
|
}, to);
|
||||||
timer = {args};
|
timer = {args, id};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +49,7 @@ function sumreduce(p: number, c: number) {
|
|||||||
return p + c;
|
return p + c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sum(arr: any[]) {
|
export function sum(arr: number[]) {
|
||||||
return arr.reduce(sumreduce, 0);
|
return arr.reduce(sumreduce, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
util/additional.types
Normal 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;
|
||||||
|
}
|
@ -26,7 +26,8 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
|||||||
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
||||||
IGNORED = set((".DS_Store", "Thumbs.db"))
|
IGNORED = set((".DS_Store", "Thumbs.db"))
|
||||||
|
|
||||||
PERM_IGNORED_FX = set(("downloads.shelf",))
|
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
|
||||||
|
PERM_IGNORED_CHROME = set(("menus", "sessions"))
|
||||||
|
|
||||||
SCRIPTS = [
|
SCRIPTS = [
|
||||||
"yarn build:regexps",
|
"yarn build:regexps",
|
||||||
@ -90,8 +91,6 @@ def build_firefox(args):
|
|||||||
else:
|
else:
|
||||||
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
|
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_FX]
|
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"
|
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-fx.zip"
|
||||||
if not out.parent.exists():
|
if not out.parent.exists():
|
||||||
@ -101,6 +100,33 @@ def build_firefox(args):
|
|||||||
print("Output", out)
|
print("Output", out)
|
||||||
build(out, json.dumps(infos, indent=2).encode("utf-8"))
|
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():
|
def main():
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
args = ArgumentParser()
|
args = ArgumentParser()
|
||||||
@ -114,6 +140,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
run([script], shell=True)
|
run([script], shell=True)
|
||||||
build_firefox(args)
|
build_firefox(args)
|
||||||
|
build_chrome(args)
|
||||||
print("DONE.")
|
print("DONE.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
25
util/i18ntochrome.py
Executable 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
@ -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
@ -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
@ -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)
|
@ -25,13 +25,14 @@ module.exports = {
|
|||||||
"select": "./windows/select.ts",
|
"select": "./windows/select.ts",
|
||||||
"single": "./windows/single.ts",
|
"single": "./windows/single.ts",
|
||||||
"prefs": "./windows/prefs.ts",
|
"prefs": "./windows/prefs.ts",
|
||||||
|
"content-popup": "./windows/popup.ts",
|
||||||
"content-gather": "./scripts/gather.ts",
|
"content-gather": "./scripts/gather.ts",
|
||||||
},
|
},
|
||||||
externals(context, request, callback) {
|
externals(context, request, callback) {
|
||||||
if (request === "crypto") {
|
if (request === "crypto") {
|
||||||
return callback(null, "crypto");
|
return callback(null, "crypto");
|
||||||
}
|
}
|
||||||
if (request.includes("_locales")) {
|
if (/_locales.*messages\.json/.test(request)) {
|
||||||
return callback(null, "null");
|
return callback(null, "null");
|
||||||
}
|
}
|
||||||
return callback();
|
return callback();
|
||||||
@ -41,6 +42,11 @@ module.exports = {
|
|||||||
filename: "[name].js"
|
filename: "[name].js"
|
||||||
},
|
},
|
||||||
devtool: "source-map",
|
devtool: "source-map",
|
||||||
|
stats: {
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
maxModules: 2,
|
||||||
|
},
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /node_modules|bundles/
|
ignored: /node_modules|bundles/
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import {Keys} from "./keys";
|
import { Keys } from "./keys";
|
||||||
|
import { $ } from "./winutil";
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
export class Broadcaster {
|
export class Broadcaster {
|
||||||
private readonly els: HTMLElement[];
|
private readonly els: HTMLElement[];
|
||||||
@ -38,7 +37,7 @@ export class Broadcaster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onkey(evt: KeyboardEvent) {
|
onkey(evt: KeyboardEvent) {
|
||||||
const {localName} = evt.target as HTMLElement;
|
const { localName } = evt.target as HTMLElement;
|
||||||
if (localName === "input" || localName === "textarea") {
|
if (localName === "input" || localName === "textarea") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
22
windows/contextmenu.ts
Normal 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));
|
||||||
|
});
|
@ -13,7 +13,7 @@ export class Dropdown extends EventEmitter {
|
|||||||
|
|
||||||
select: HTMLSelectElement;
|
select: HTMLSelectElement;
|
||||||
|
|
||||||
constructor(el: string, options: any[] = []) {
|
constructor(el: string, options: string[] = []) {
|
||||||
super();
|
super();
|
||||||
let input = document.querySelector(el);
|
let input = document.querySelector(el);
|
||||||
if (!input || !input.parentElement) {
|
if (!input || !input.parentElement) {
|
||||||
|
@ -19,7 +19,7 @@ export class Icons extends Map {
|
|||||||
}
|
}
|
||||||
let cls = super.get(url);
|
let cls = super.get(url);
|
||||||
if (!cls) {
|
if (!cls) {
|
||||||
cls = `icon-${++this.running}`;
|
cls = `iconcache-${++this.running}`;
|
||||||
const rule = `.${cls} { background-image: url(${url}); }`;
|
const rule = `.${cls} { background-image: url(${url}); }`;
|
||||||
this.sheet.insertRule(rule);
|
this.sheet.insertRule(rule);
|
||||||
super.set(url, cls);
|
super.set(url, cls);
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import {EventEmitter} from "../lib/events";
|
import {EventEmitter} from "../lib/events";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { ContextMenu } from "../uikit/lib/contextmenu";
|
import { ContextMenu } from "./contextmenu";
|
||||||
import { runtime } from "../lib/browser";
|
import { runtime } from "../lib/browser";
|
||||||
|
|
||||||
export const Keys = new class extends EventEmitter {
|
export const Keys = new class extends EventEmitter {
|
||||||
|
@ -128,8 +128,8 @@
|
|||||||
<template id="menufilter-template">
|
<template id="menufilter-template">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="ctx-menufilter-seperator">-</li>
|
<li id="ctx-menufilter-seperator">-</li>
|
||||||
<li id="ctx-menufilter-invert" data-autohide="false">Invert</li>
|
<li id="ctx-menufilter-invert" data-auto-hide="false">Invert</li>
|
||||||
<li id="ctx-menufilter-clear" data-autohide="false">Clear</li>
|
<li id="ctx-menufilter-clear" data-auto-hide="false">Clear</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
<li id="ctx-menufilter-sort-ascending" data-icon="icon-sort-asc">Sort ascending</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>
|
<li id="ctx-menufilter-sort-descending" data-icon="icon-sort-desc">Sort descending</li>
|
||||||
|